WordPress 4.7
[autoinstalls/wordpress.git] / wp-includes / class-wp-customize-nav-menus.php
index f9832a69b2f164c4713e7eaa54f0a40ec3a58666..ca1b72f7eb788aa04125466f89a1fdb201edb7ab 100644 (file)
@@ -48,7 +48,12 @@ final class WP_Customize_Nav_Menus {
                $this->previewed_menus = array();
                $this->manager         = $manager;
 
-               // Skip useless hooks when the user can't manage nav menus anyway.
+               // 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 );
+
+               // Skip remaining hooks when the user can't manage nav menus anyway.
                if ( ! current_user_can( 'edit_theme_options' ) ) {
                        return;
                }
@@ -56,16 +61,13 @@ final class WP_Customize_Nav_Menus {
                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' ) );
-
-               // Needs to run after core Navigation section is set up.
-               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_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' ) );
+               add_action( 'customize_save_nav_menus_created_posts', array( $this, 'save_nav_menus_created_posts' ) );
 
                // Selective Refresh partials.
                add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
@@ -98,20 +100,35 @@ final class WP_Customize_Nav_Menus {
                        wp_die( -1 );
                }
 
-               if ( empty( $_POST['type'] ) || empty( $_POST['object'] ) ) {
+               $all_items = array();
+               $item_types = array();
+               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.
+                       $item_types[] = array(
+                               'type' => wp_unslash( $_POST['type'] ),
+                               'object' => wp_unslash( $_POST['object'] ),
+                               'page' => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ),
+                       );
+               } else {
                        wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
                }
 
-               $type = sanitize_key( $_POST['type'] );
-               $object = sanitize_key( $_POST['object'] );
-               $page = empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] );
-               $items = $this->load_available_items_query( $type, $object, $page );
-
-               if ( is_wp_error( $items ) ) {
-                       wp_send_json_error( $items->get_error_code() );
-               } else {
-                       wp_send_json_success( array( 'items' => $items ) );
+               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 ) );
        }
 
        /**
@@ -157,13 +174,25 @@ final class WP_Customize_Nav_Menus {
                                );
                        }
 
-                       $posts = get_posts( array(
+                       // Prepend posts with nav_menus_created_posts on first page.
+                       $posts = array();
+                       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;
+                                       }
+                               }
+                       }
+
+                       $posts = array_merge( $posts, get_posts( array(
                                'numberposts' => 10,
                                'offset'      => 10 * $page,
                                'orderby'     => 'date',
                                'order'       => 'DESC',
                                'post_type'   => $object,
-                       ) );
+                       ) ) );
+
                        foreach ( $posts as $post ) {
                                $post_title = $post->post_title;
                                if ( '' === $post_title ) {
@@ -288,27 +317,42 @@ final class WP_Customize_Nav_Menus {
                        $query['s'] = $args['s'];
                }
 
+               $posts = array();
+
+               // 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 ) > 0 ) {
+                       $stub_post_query = new WP_Query( array_merge(
+                               $query,
+                               array(
+                                       'post_status' => 'auto-draft',
+                                       'post__in' => $nav_menus_created_posts_setting->value(),
+                                       'posts_per_page' => -1,
+                               )
+                       ) );
+                       $posts = array_merge( $posts, $stub_post_query->posts );
+               }
+
                // Query posts.
                $get_posts = new WP_Query( $query );
-
-               // Check if any posts were found.
-               if ( $get_posts->post_count ) {
-                       foreach ( $get_posts->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 );
-                               }
-                               $items[] = array(
-                                       'id'         => 'post-' . $post->ID,
-                                       'title'      => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
-                                       'type'       => 'post_type',
-                                       'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
-                                       'object'     => $post->post_type,
-                                       'object_id'  => intval( $post->ID ),
-                                       'url'        => get_permalink( intval( $post->ID ) ),
-                               );
+               $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 );
                        }
+                       $items[] = array(
+                               'id'         => 'post-' . $post->ID,
+                               'title'      => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
+                               'type'       => 'post_type',
+                               'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
+                               'object'     => $post->post_type,
+                               'object_id'  => intval( $post->ID ),
+                               'url'        => get_permalink( intval( $post->ID ) ),
+                       );
                }
 
                // Query taxonomy terms.
@@ -368,6 +412,7 @@ final class WP_Customize_Nav_Menus {
                                'untitled'          => _x( '(no label)', 'missing menu item navigation label' ),
                                'unnamed'           => _x( '(unnamed)', 'Missing menu name.' ),
                                'custom_label'      => __( 'Custom Link' ),
+                               'page_label'        => get_post_type_object( 'page' )->labels->singular_name,
                                /* translators: %s: menu location */
                                'menuLocation'      => _x( '(Currently set to: %s)', 'menu' ),
                                'menuNameLabel'     => __( 'Menu Name' ),
@@ -486,6 +531,24 @@ final class WP_Customize_Nav_Menus {
         */
        public function customize_register() {
 
+               /*
+                * Preview settings for nav menus early so that the sections and controls will be added properly.
+                * See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L506-L543
+                */
+               $nav_menus_setting_ids = array();
+               foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) {
+                       if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) {
+                               $nav_menus_setting_ids[] = $setting_id;
+                       }
+               }
+               $this->manager->add_dynamic_settings( $nav_menus_setting_ids );
+               foreach ( $nav_menus_setting_ids as $setting_id ) {
+                       $setting = $this->manager->get_setting( $setting_id );
+                       if ( $setting ) {
+                               $setting->preview();
+                       }
+               }
+
                // Require JS-rendered control types.
                $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' );
                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
@@ -587,6 +650,10 @@ final class WP_Customize_Nav_Menus {
                                $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
 
                                $value = (array) $item;
+                               if ( empty( $value['post_title'] ) ) {
+                                       $value['title'] = '';
+                               }
+
                                $value['nav_menu_term_id'] = $menu_id;
                                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
                                        'value'     => $value,
@@ -626,6 +693,13 @@ final class WP_Customize_Nav_Menus {
                        'section'  => 'add_menu',
                        'settings' => array(),
                ) ) );
+
+               $this->manager->add_setting( new WP_Customize_Filter_Setting( $this->manager, 'nav_menus_created_posts', array(
+                       'transport' => 'postMessage',
+                       'type' => 'option', // To prevent theme prefix in changeset.
+                       'default' => array(),
+                       'sanitize_callback' => array( $this, 'sanitize_nav_menus_created_posts' ),
+               ) ) );
        }
 
        /**
@@ -648,6 +722,7 @@ final class WP_Customize_Nav_Menus {
         * Return an array of all the available item types.
         *
         * @since 4.3.0
+        * @since 4.7.0  Each array item now includes a `$type_label` in in addition to `$title`, `$type`, and `$object`.
         * @access public
         *
         * @return array The available menu item types.
@@ -660,7 +735,8 @@ final class WP_Customize_Nav_Menus {
                        foreach ( $post_types as $slug => $post_type ) {
                                $item_types[] = array(
                                        'title'  => $post_type->labels->name,
-                                       'type'   => 'post_type',
+                                       'type_label' => $post_type->labels->singular_name,
+                                       'type' => 'post_type',
                                        'object' => $post_type->name,
                                );
                        }
@@ -673,8 +749,9 @@ final class WP_Customize_Nav_Menus {
                                        continue;
                                }
                                $item_types[] = array(
-                                       'title'  => $taxonomy->labels->name,
-                                       'type'   => 'taxonomy',
+                                       'title' => $taxonomy->labels->name,
+                                       'type_label' => $taxonomy->labels->singular_name,
+                                       'type' => 'taxonomy',
                                        'object' => $taxonomy->name,
                                );
                        }
@@ -684,6 +761,7 @@ final class WP_Customize_Nav_Menus {
                 * Filters the available menu item types.
                 *
                 * @since 4.3.0
+                * @since 4.7.0  Each array item now includes a `$type_label` in in addition to `$title`, `$type`, and `$object`.
                 *
                 * @param array $item_types Custom menu item types.
                 */
@@ -692,6 +770,130 @@ final class WP_Customize_Nav_Menus {
                return $item_types;
        }
 
+       /**
+        * Add a new `auto-draft` post.
+        *
+        * @access public
+        * @since 4.7.0
+        *
+        * @param array $postarr {
+        *     Post array. Note that post_status is overridden to be `auto-draft`.
+        *
+        *     @var string $post_title   Post title. Required.
+        *     @var string $post_type    Post type. Required.
+        *     @var string $post_name    Post name.
+        *     @var string $post_content Post content.
+        * }
+        * @return WP_Post|WP_Error Inserted auto-draft post object or error.
+        */
+       public function insert_auto_draft_post( $postarr ) {
+               if ( ! isset( $postarr['post_type'] ) || ! post_type_exists( $postarr['post_type'] )  ) {
+                       return new WP_Error( 'unknown_post_type', __( 'Unknown post type' ) );
+               }
+               if ( empty( $postarr['post_title'] ) ) {
+                       return new WP_Error( 'empty_title', __( 'Empty title' ) );
+               }
+               if ( ! empty( $postarr['post_status'] ) ) {
+                       return new WP_Error( 'status_forbidden', __( 'Status is forbidden' ) );
+               }
+
+               $postarr['post_status'] = 'auto-draft';
+
+               // Auto-drafts are allowed to have empty post_names, so it has to be explicitly set.
+               if ( empty( $postarr['post_name'] ) ) {
+                       $postarr['post_name'] = sanitize_title( $postarr['post_title'] );
+               }
+               if ( ! isset( $postarr['meta_input'] ) ) {
+                       $postarr['meta_input'] = array();
+               }
+               $postarr['meta_input']['_customize_draft_post_name'] = $postarr['post_name'];
+               unset( $postarr['post_name'] );
+
+               add_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
+               $r = wp_insert_post( wp_slash( $postarr ), true );
+               remove_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
+
+               if ( is_wp_error( $r ) ) {
+                       return $r;
+               } else {
+                       return get_post( $r );
+               }
+       }
+
+       /**
+        * Ajax handler for adding a new auto-draft post.
+        *
+        * @access public
+        * @since 4.7.0
+        */
+       public function ajax_insert_auto_draft_post() {
+               if ( ! check_ajax_referer( 'customize-menus', 'customize-menus-nonce', false ) ) {
+                       wp_send_json_error( 'bad_nonce', 400 );
+               }
+
+               if ( ! current_user_can( 'customize' ) ) {
+                       wp_send_json_error( 'customize_not_allowed', 403 );
+               }
+
+               if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
+                       wp_send_json_error( 'missing_params', 400 );
+               }
+
+               $params = wp_unslash( $_POST['params'] );
+               $illegal_params = array_diff( array_keys( $params ), array( 'post_type', 'post_title' ) );
+               if ( ! empty( $illegal_params ) ) {
+                       wp_send_json_error( 'illegal_params', 400 );
+               }
+
+               $params = array_merge(
+                       array(
+                               'post_type' => '',
+                               'post_title' => '',
+                       ),
+                       $params
+               );
+
+               if ( empty( $params['post_type'] ) || ! post_type_exists( $params['post_type'] ) ) {
+                       status_header( 400 );
+                       wp_send_json_error( 'missing_post_type_param' );
+               }
+
+               $post_type_object = get_post_type_object( $params['post_type'] );
+               if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
+                       status_header( 403 );
+                       wp_send_json_error( 'insufficient_post_permissions' );
+               }
+
+               $params['post_title'] = trim( $params['post_title'] );
+               if ( '' === $params['post_title'] ) {
+                       status_header( 400 );
+                       wp_send_json_error( 'missing_post_title' );
+               }
+
+               $r = $this->insert_auto_draft_post( $params );
+               if ( is_wp_error( $r ) ) {
+                       $error = $r;
+                       if ( ! empty( $post_type_object->labels->singular_name ) ) {
+                               $singular_name = $post_type_object->labels->singular_name;
+                       } else {
+                               $singular_name = __( 'Post' );
+                       }
+
+                       $data = array(
+                               /* translators: %1$s is the post type name and %2$s is the error message. */
+                               'message' => sprintf( __( '%1$s could not be created: %2$s' ), $singular_name, $error->get_error_message() ),
+                       );
+                       wp_send_json_error( $data );
+               } else {
+                       $post = $r;
+                       $data = array(
+                               'post_id' => $post->ID,
+                               'url'     => get_permalink( $post->ID ),
+                       );
+                       wp_send_json_success( $data );
+               }
+       }
+
        /**
         * Print the JavaScript templates used to render Menu Customizer components.
         *
@@ -767,61 +969,116 @@ final class WP_Customize_Nav_Menus {
                                        <p class="screen-reader-text" id="menu-items-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
                                        <span class="spinner"></span>
                                </div>
+                               <div class="search-icon" aria-hidden="true"></div>
                                <button type="button" class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></button>
-                               <ul class="accordion-section-content" data-type="search"></ul>
-                       </div>
-                       <div id="new-custom-menu-item" class="accordion-section">
-                               <h4 class="accordion-section-title" role="presentation">
-                                       <?php _e( 'Custom Links' ); ?>
-                                       <button type="button" class="button-link" aria-expanded="false">
-                                               <span class="screen-reader-text"><?php _e( 'Toggle section: Custom Links' ); ?></span>
-                                               <span class="toggle-indicator" aria-hidden="true"></span>
-                                       </button>
-                               </h4>
-                               <div class="accordion-section-content customlinkdiv">
-                                       <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
-                                       <p id="menu-item-url-wrap" class="wp-clearfix">
-                                               <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
-                                               <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
-                                       </p>
-                                       <p id="menu-item-name-wrap" class="wp-clearfix">
-                                               <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
-                                               <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
-                                       </p>
-                                       <p class="button-controls">
-                                               <span class="add-to-menu">
-                                                       <input type="submit" class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>" name="add-custom-menu-item" id="custom-menu-item-submit">
-                                                       <span class="spinner"></span>
-                                               </span>
-                                       </p>
-                               </div>
+                               <ul class="accordion-section-content available-menu-items-list" data-type="search"></ul>
                        </div>
                        <?php
-                       // Containers for per-post-type item browsing; items added with JS.
-                       foreach ( $this->available_item_types() as $available_item_type ) {
-                               $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
-                               ?>
-                               <div id="<?php echo esc_attr( $id ); ?>" class="accordion-section">
-                                       <h4 class="accordion-section-title" role="presentation">
-                                               <?php echo esc_html( $available_item_type['title'] ); ?>
-                                               <span class="spinner"></span>
-                                               <span class="no-items"><?php _e( 'No items' ); ?></span>
-                                               <button type="button" class="button-link" aria-expanded="false">
-                                                       <span class="screen-reader-text"><?php
-                                                       /* translators: %s: Title of a section with menu items */
-                                                       printf( __( 'Toggle section: %s' ), esc_html( $available_item_type['title'] ) ); ?></span>
-                                                       <span class="toggle-indicator" aria-hidden="true"></span>
-                                               </button>
-                                       </h4>
-                                       <ul class="accordion-section-content" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>"></ul>
-                               </div>
-                               <?php
+
+                       // Ensure the page post type comes first in the list.
+                       $item_types = $this->available_item_types();
+                       $page_item_type = null;
+                       foreach ( $item_types as $i => $item_type ) {
+                               if ( isset( $item_type['object'] ) && 'page' === $item_type['object'] ) {
+                                       $page_item_type = $item_type;
+                                       unset( $item_types[ $i ] );
+                               }
+                       }
+
+                       $this->print_custom_links_available_menu_item();
+                       if ( $page_item_type ) {
+                               $this->print_post_type_container( $page_item_type );
+                       }
+                       // Containers for per-post-type item browsing; items are added with JS.
+                       foreach ( $item_types as $item_type ) {
+                               $this->print_post_type_container( $item_type );
                        }
                        ?>
                </div><!-- #available-menu-items -->
        <?php
        }
 
+       /**
+        * Print the markup for new menu items.
+        *
+        * To be used in the template #available-menu-items.
+        *
+        * @since 4.7.0
+        * @access private
+        *
+        * @param array $available_item_type Menu item data to output, including title, type, and label.
+        * @return void
+        */
+       protected function print_post_type_container( $available_item_type ) {
+               $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
+               ?>
+               <div id="<?php echo esc_attr( $id ); ?>" class="accordion-section">
+                       <h4 class="accordion-section-title" role="presentation">
+                               <?php echo esc_html( $available_item_type['title'] ); ?>
+                               <span class="spinner"></span>
+                               <span class="no-items"><?php _e( 'No items' ); ?></span>
+                               <button type="button" class="button-link" aria-expanded="false">
+                                       <span class="screen-reader-text"><?php
+                                               /* translators: %s: Title of a section with menu items */
+                                               printf( __( 'Toggle section: %s' ), esc_html( $available_item_type['title'] ) ); ?></span>
+                                       <span class="toggle-indicator" aria-hidden="true"></span>
+                               </button>
+                       </h4>
+                       <div class="accordion-section-content">
+                               <?php if ( 'post_type' === $available_item_type['type'] ) : ?>
+                                       <?php $post_type_obj = get_post_type_object( $available_item_type['object'] ); ?>
+                                       <?php if ( current_user_can( $post_type_obj->cap->create_posts ) && current_user_can( $post_type_obj->cap->publish_posts ) ) : ?>
+                                               <div class="new-content-item">
+                                                       <input type="text" class="create-item-input" placeholder="<?php echo esc_attr( $post_type_obj->labels->add_new_item ); ?>">
+                                                       <button type="button" class="button add-content"><?php _e( 'Add' ); ?></button>
+                                               </div>
+                                       <?php endif; ?>
+                               <?php endif; ?>
+                               <ul class="available-menu-items-list" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>" data-type_label="<?php echo esc_attr( isset( $available_item_type['type_label'] ) ? $available_item_type['type_label'] : $available_item_type['type'] ); ?>"></ul>
+                       </div>
+               </div>
+               <?php
+       }
+
+       /**
+        * Print the markup for available menu item custom links.
+        *
+        * @since 4.7.0
+        * @access private
+        *
+        * @return void
+        */
+       protected function print_custom_links_available_menu_item() {
+               ?>
+               <div id="new-custom-menu-item" class="accordion-section">
+                       <h4 class="accordion-section-title" role="presentation">
+                               <?php _e( 'Custom Links' ); ?>
+                               <button type="button" class="button-link" aria-expanded="false">
+                                       <span class="screen-reader-text"><?php _e( 'Toggle section: Custom Links' ); ?></span>
+                                       <span class="toggle-indicator" aria-hidden="true"></span>
+                               </button>
+                       </h4>
+                       <div class="accordion-section-content customlinkdiv">
+                               <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
+                               <p id="menu-item-url-wrap" class="wp-clearfix">
+                                       <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
+                                       <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
+                               </p>
+                               <p id="menu-item-name-wrap" class="wp-clearfix">
+                                       <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
+                                       <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
+                               </p>
+                               <p class="button-controls">
+                                       <span class="add-to-menu">
+                                               <input type="submit" class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>" name="add-custom-menu-item" id="custom-menu-item-submit">
+                                               <span class="spinner"></span>
+                                       </span>
+                               </p>
+                       </div>
+               </div>
+               <?php
+       }
+
        //
        // Start functionality specific to partial-refresh of menu changes in Customizer preview.
        //
@@ -880,6 +1137,83 @@ final class WP_Customize_Nav_Menus {
                add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
        }
 
+       /**
+        * Make the auto-draft status protected so that it can be queried.
+        *
+        * @since 4.7.0
+        * @access public
+        */
+       public function make_auto_draft_status_previewable() {
+               global $wp_post_statuses;
+               $wp_post_statuses['auto-draft']->protected = true;
+       }
+
+       /**
+        * Sanitize post IDs for auto-draft posts created for nav menu items to be published.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @param array $value Post IDs.
+        * @returns array Post IDs.
+        */
+       public function sanitize_nav_menus_created_posts( $value ) {
+               $post_ids = array();
+               foreach ( wp_parse_id_list( $value ) as $post_id ) {
+                       if ( empty( $post_id ) ) {
+                               continue;
+                       }
+                       $post = get_post( $post_id );
+                       if ( 'auto-draft' !== $post->post_status ) {
+                               continue;
+                       }
+                       $post_type_obj = get_post_type_object( $post->post_type );
+                       if ( ! $post_type_obj ) {
+                               continue;
+                       }
+                       if ( ! current_user_can( $post_type_obj->cap->publish_posts ) || ! current_user_can( $post_type_obj->cap->edit_post, $post_id ) ) {
+                               continue;
+                       }
+                       $post_ids[] = $post->ID;
+               }
+               return $post_ids;
+       }
+
+       /**
+        * Publish the auto-draft posts that were created for nav menu items.
+        *
+        * The post IDs will have been sanitized by already by
+        * `WP_Customize_Nav_Menu_Items::sanitize_nav_menus_created_posts()` to
+        * remove any post IDs for which the user cannot publish or for which the
+        * post is not an auto-draft.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @param WP_Customize_Setting $setting Customizer setting object.
+        */
+       public function save_nav_menus_created_posts( $setting ) {
+               $post_ids = $setting->post_value();
+               if ( ! empty( $post_ids ) ) {
+                       foreach ( $post_ids as $post_id ) {
+                               $target_status = 'attachment' === get_post_type( $post_id ) ? 'inherit' : 'publish';
+                               $args = array(
+                                       'ID' => $post_id,
+                                       'post_status' => $target_status,
+                               );
+                               $post_name = get_post_meta( $post_id, '_customize_draft_post_name', true );
+                               if ( $post_name ) {
+                                       $args['post_name'] = $post_name;
+                               }
+
+                               // Note that wp_publish_post() cannot be used because unique slugs need to be assigned.
+                               wp_update_post( wp_slash( $args ) );
+
+                               delete_post_meta( $post_id, '_customize_draft_post_name' );
+                       }
+               }
+       }
+
        /**
         * Keep track of the arguments that are being passed to wp_nav_menu().
         *