diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css
index d2eef49a5b..af62e5e23e 100644
--- a/src/wp-admin/css/customize-controls.css
+++ b/src/wp-admin/css/customize-controls.css
@@ -1219,7 +1219,8 @@ body.cheatin p {
}
.add-new-widget:before,
-.add-new-menu-item:before {
+.add-new-menu-item:before,
+#available-menu-items .new-content-item .add-content:before {
content: "\f132";
display: inline-block;
position: relative;
diff --git a/src/wp-admin/css/customize-nav-menus.css b/src/wp-admin/css/customize-nav-menus.css
index afe0cbc566..66d2b08982 100644
--- a/src/wp-admin/css/customize-nav-menus.css
+++ b/src/wp-admin/css/customize-nav-menus.css
@@ -61,6 +61,10 @@
text-align: right;
}
+.customize-control-nav_menu_item.has-notifications .menu-item-handle {
+ border-left: 4px solid #00a0d2;
+}
+
.wp-customizer .menu-item-settings {
max-width: 100%;
overflow: hidden;
@@ -497,7 +501,7 @@
color: #23282d;
}
-#available-menu-items .accordion-section-content {
+#available-menu-items .available-menu-items-list {
overflow-y: auto;
max-height: 200px; /* This gets set in JS to fit the screen size, and based on # of sections. */
background: transparent;
@@ -534,9 +538,58 @@
}
#available-menu-items .accordion-section-content {
- padding: 1px 15px 15px 15px;
- margin: 0;
max-height: 290px;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ background: transparent;
+}
+
+#available-menu-items .accordion-section-content .available-menu-items-list {
+ margin: 0 0 45px 0;
+ padding: 1px 15px 15px 15px;
+}
+
+#available-menu-items .accordion-section-content .available-menu-items-list:only-child { /* Types that do not support new items for the current user */
+ margin-bottom: 0;
+}
+
+#new-custom-menu-item .accordion-section-content {
+ padding: 0 15px 15px 15px;
+}
+
+#available-menu-items .accordion-section-content .new-content-item {
+ width: calc(100% - 30px);
+ padding: 8px 15px;
+ position: absolute;
+ bottom: 0;
+ z-index: 10;
+ background: #eee;
+ display: -webkit-box;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+}
+
+#available-menu-items .new-content-item .create-item-input {
+ -webkit-box-flex: 10;
+ -webkit-flex-grow: 10;
+ -moz-box-flex: 10;
+ -ms-flex-positive: 10;
+ -ms-flex: 10;
+ flex-grow: 10;
+ margin-left: 5px;
+ padding: 4.5px;
+}
+#available-menu-items .new-content-item .add-content {
+ padding-left: 6px;
+ -webkit-box-flex: 10;
+ -webkit-flex-grow: 10;
+ -moz-box-flex: 10;
+ -ms-flex-positive: 10;
+ -ms-flex: 10;
+ flex-grow: 1;
}
#available-menu-items .menu-item-tpl {
@@ -546,7 +599,9 @@
#custom-menu-item-name.invalid,
#custom-menu-item-url.invalid,
.menu-name-field.invalid,
-.menu-name-field.invalid:focus {
+.menu-name-field.invalid:focus,
+#available-menu-items .new-content-item .create-item-input.invalid,
+#available-menu-items .new-content-item .create-item-input.invalid:focus {
border: 1px solid #f00;
}
diff --git a/src/wp-admin/js/customize-nav-menus.js b/src/wp-admin/js/customize-nav-menus.js
index 25495feeb6..e514a0ad9f 100644
--- a/src/wp-admin/js/customize-nav-menus.js
+++ b/src/wp-admin/js/customize-nav-menus.js
@@ -80,6 +80,47 @@
});
api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
+ api.Menus.insertedAutoDrafts = [];
+
+ /**
+ * Insert a new `auto-draft` post.
+ *
+ * @param {object} params - Parameters for the draft post to create.
+ * @param {string} params.post_type - Post type to add.
+ * @param {string} params.post_title - Post title to use.
+ * @return {jQuery.promise} Promise resolved with the added post.
+ */
+ api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
+ var request, deferred = $.Deferred();
+
+ request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
+ 'customize-menus-nonce': api.settings.nonce['customize-menus'],
+ 'wp_customize': 'on',
+ 'params': params
+ } );
+
+ request.done( function( response ) {
+ if ( response.post_id ) {
+ deferred.resolve( response );
+ api.Menus.insertedAutoDrafts.push( response.post_id );
+ api( 'nav_menus_created_posts' ).set( _.clone( api.Menus.insertedAutoDrafts ) );
+ }
+ } );
+
+ request.fail( function( response ) {
+ var error = response || '';
+
+ if ( 'undefined' !== typeof response.message ) {
+ error = response.message;
+ }
+
+ console.error( error );
+ deferred.rejectWith( error );
+ } );
+
+ return deferred.promise();
+ };
+
/**
* wp.customize.Menus.AvailableMenuItemsPanelView
*
@@ -100,6 +141,8 @@
'click .menu-item-tpl': '_submit',
'click #custom-menu-item-submit': '_submitLink',
'keypress #custom-menu-item-name': '_submitLink',
+ 'click .new-content-item .add-content': '_submitNew',
+ 'keypress .create-item-input': '_submitNew',
'keydown': 'keyboardAccessible'
},
@@ -115,6 +158,7 @@
pages: {},
sectionContent: '',
loading: false,
+ addingNew: false,
initialize: function() {
var self = this;
@@ -124,7 +168,7 @@
}
this.$search = $( '#menu-items-search' );
- this.sectionContent = this.$el.find( '.accordion-section-content' );
+ this.sectionContent = this.$el.find( '.available-menu-items-list' );
this.debounceSearch = _.debounce( self.search, 500 );
@@ -160,7 +204,7 @@
// Load more items.
this.sectionContent.scroll( function() {
- var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ),
+ var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
visibleHeight = self.$el.find( '.accordion-section.open' ).height();
if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
@@ -337,7 +381,7 @@
}
items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
self.collection.add( items.models );
- typeInner = availableMenuItemContainer.find( '.accordion-section-content' );
+ typeInner = availableMenuItemContainer.find( '.available-menu-items-list' );
items.each(function( menuItem ) {
typeInner.append( itemTemplate( menuItem.attributes ) );
});
@@ -356,13 +400,15 @@
// Adjust the height of each section of items to fit the screen.
itemSectionHeight: function() {
- var sections, totalHeight, accordionHeight, diff;
+ var sections, lists, totalHeight, accordionHeight, diff;
totalHeight = window.innerHeight;
sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
- accordionHeight = 46 * ( 2 + sections.length ) - 13; // Magic numbers.
+ lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
+ accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
diff = totalHeight - accordionHeight;
if ( 120 < diff && 290 > diff ) {
sections.css( 'max-height', diff );
+ lists.css( 'max-height', ( diff - 60 ) );
}
},
@@ -456,6 +502,88 @@
itemName.val( '' );
},
+ // Submit handler for keypress (enter) on field and click on button.
+ _submitNew: function( event ) {
+ var container;
+
+ // Only proceed with keypress if it is Enter.
+ if ( 'keypress' === event.type && 13 !== event.which ) {
+ return;
+ }
+
+ if ( this.addingNew ) {
+ return;
+ }
+
+ container = $( event.target ).closest( '.accordion-section' );
+
+ this.submitNew( container );
+ },
+
+ // Creates a new object and adds an associated menu item to the menu.
+ submitNew: function( container ) {
+ var panel = this,
+ itemName = container.find( '.create-item-input' ),
+ title = itemName.val(),
+ dataContainer = container.find( '.available-menu-items-list' ),
+ itemType = dataContainer.data( 'type' ),
+ itemObject = dataContainer.data( 'object' ),
+ itemTypeLabel = dataContainer.data( 'type_label' ),
+ promise;
+
+ if ( ! this.currentMenuControl ) {
+ return;
+ }
+
+ // Only posts are supported currently.
+ if ( 'post_type' !== itemType ) {
+ return;
+ }
+
+ if ( '' === $.trim( itemName.val() ) ) {
+ itemName.addClass( 'invalid' );
+ itemName.focus();
+ return;
+ } else {
+ itemName.removeClass( 'invalid' );
+ container.find( '.accordion-section-title' ).addClass( 'loading' );
+ }
+
+ panel.addingNew = true;
+ itemName.attr( 'disabled', 'disabled' );
+ promise = api.Menus.insertAutoDraftPost( {
+ post_title: title,
+ post_type: itemObject
+ } );
+ promise.done( function( data ) {
+ var availableItem, $content, itemTemplate;
+ availableItem = new api.Menus.AvailableItemModel( {
+ 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
+ 'title': itemName.val(),
+ 'type': itemType,
+ 'type_label': itemTypeLabel,
+ 'object': itemObject,
+ 'object_id': data.post_id,
+ 'url': data.url
+ } );
+
+ // Add new item to menu.
+ panel.currentMenuControl.addItemToMenu( availableItem.attributes );
+
+ // Add the new item to the list of available items.
+ api.Menus.availableMenuItemsPanel.collection.add( availableItem );
+ $content = container.find( '.available-menu-items-list' );
+ itemTemplate = wp.template( 'available-menu-item' );
+ $content.prepend( itemTemplate( availableItem.attributes ) );
+ $content.scrollTop();
+
+ // Reset the create content form.
+ itemName.val( '' ).removeAttr( 'disabled' );
+ panel.addingNew = false;
+ container.find( '.accordion-section-title' ).removeClass( 'loading' );
+ } );
+ },
+
// Opens the panel.
open: function( menuControl ) {
this.currentMenuControl = menuControl;
@@ -2545,6 +2673,9 @@
if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
api.Menus.applySavedData( data );
}
+
+ // Reset list of inserted auto draft post IDs.
+ api.Menus.insertedAutoDrafts = [];
} );
// Open and focus menu control.
diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php
index f9832a69b2..898152d6c7 100644
--- a/src/wp-includes/class-wp-customize-nav-menus.php
+++ b/src/wp-includes/class-wp-customize-nav-menus.php
@@ -56,16 +56,16 @@ 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 );
@@ -626,6 +626,12 @@ 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',
+ 'default' => array(),
+ 'sanitize_callback' => array( $this, 'sanitize_nav_menus_created_posts' ),
+ ) ) );
}
/**
@@ -648,6 +654,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 +667,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 +681,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 +693,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 +702,119 @@ final class WP_Customize_Nav_Menus {
return $item_types;
}
+ /**
+ * Add a new `auto-draft` post.
+ *
+ * @access public
+ * @since 4.7.0
+ *
+ * @param array $postarr {
+ * Abbreviated post array.
+ *
+ * @var string $post_title Post title.
+ * @var string $post_type Post type.
+ * }
+ * @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 ( ! isset( $postarr['post_title'] ) ) {
+ $postarr['post_title'] = '';
+ }
+
+ add_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
+ $args = array(
+ 'post_status' => 'auto-draft',
+ 'post_type' => $postarr['post_type'],
+ 'post_title' => $postarr['post_title'],
+ 'post_name' => sanitize_title( $postarr['post_title'] ), // Auto-drafts are allowed to have empty post_names, so we need to explicitly set it.
+ );
+ $r = wp_insert_post( wp_slash( $args ), 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 ) ) {
+ status_header( 400 );
+ wp_send_json_error( 'bad_nonce' );
+ }
+
+ if ( ! current_user_can( 'customize' ) ) {
+ status_header( 403 );
+ wp_send_json_error( 'customize_not_allowed' );
+ }
+
+ if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
+ status_header( 400 );
+ wp_send_json_error( 'missing_params' );
+ }
+
+ $params = wp_array_slice_assoc(
+ array_merge(
+ array(
+ 'post_type' => '',
+ 'post_title' => '',
+ ),
+ wp_unslash( $_POST['params'] )
+ ),
+ array( 'post_type', 'post_title' )
+ );
+
+ 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.
*
@@ -768,7 +891,7 @@ final class WP_Customize_Nav_Menus {
-
+
available_item_types() as $available_item_type ) {
$id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
?>
@@ -813,7 +937,20 @@ final class WP_Customize_Nav_Menus {
-
+
+
+
+ cap->create_posts ) && current_user_can( $post_type_obj->cap->publish_posts ) ) : ?>
+
+
+
+
+
+
+
+
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 ) {
+ wp_publish_post( $post_id );
+ }
+ }
+ }
+
/**
* Keep track of the arguments that are being passed to wp_nav_menu().
*
diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php
index f0fe8f0b12..829b8e90d8 100644
--- a/src/wp-includes/class-wp-customize-setting.php
+++ b/src/wp-includes/class-wp-customize-setting.php
@@ -498,6 +498,8 @@ class WP_Customize_Setting {
/**
* Fetch and sanitize the $_POST value for the setting.
*
+ * During a save request prior to save, post_value() provides the new value while value() does not.
+ *
* @since 3.4.0
*
* @param mixed $default A default value which is used as a fallback. Default is null.
diff --git a/tests/phpunit/tests/ajax/CustomizeMenus.php b/tests/phpunit/tests/ajax/CustomizeMenus.php
index 025d29f6a6..28871b749b 100644
--- a/tests/phpunit/tests/ajax/CustomizeMenus.php
+++ b/tests/phpunit/tests/ajax/CustomizeMenus.php
@@ -526,4 +526,114 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase {
),
);
}
+
+ /**
+ * Testing successful ajax_insert_auto_draft_post() call.
+ *
+ * @covers WP_Customize_Nav_Menus::ajax_insert_auto_draft_post()
+ */
+ function test_ajax_insert_auto_draft_post_success() {
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ),
+ 'params' => array(
+ 'post_type' => 'post',
+ 'post_title' => 'Hello World',
+ ),
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+
+ $this->assertTrue( $response['success'] );
+ $this->assertArrayHasKey( 'post_id', $response['data'] );
+ $this->assertArrayHasKey( 'url', $response['data'] );
+ }
+
+ /**
+ * Testing unsuccessful ajax_insert_auto_draft_post() call.
+ *
+ * @covers WP_Customize_Nav_Menus::ajax_insert_auto_draft_post()
+ */
+ function test_ajax_insert_auto_draft_failures() {
+ // No nonce.
+ $_POST = array();
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'bad_nonce', $response['data'] );
+
+ // Bad nonce.
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => 'bad',
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'bad_nonce', $response['data'] );
+
+ // Bad nonce.
+ wp_set_current_user( $this->factory()->user->create( array( 'role' => 'subscriber' ) ) );
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ),
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'customize_not_allowed', $response['data'] );
+
+ // Missing params.
+ wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ),
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'missing_params', $response['data'] );
+
+ // insufficient_post_permissions.
+ register_post_type( 'privilege', array( 'capability_type' => 'privilege' ) );
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ),
+ 'params' => array(
+ 'post_type' => 'privilege',
+ ),
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'insufficient_post_permissions', $response['data'] );
+
+ // insufficient_post_permissions.
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ),
+ 'params' => array(
+ 'post_type' => 'non-existent',
+ ),
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'missing_post_type_param', $response['data'] );
+
+ // missing_post_title.
+ $_POST = wp_slash( array(
+ 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ),
+ 'params' => array(
+ 'post_type' => 'post',
+ 'post_title' => ' ',
+ ),
+ ) );
+ $this->_last_response = '';
+ $this->make_ajax_call( 'customize-nav-menus-insert-auto-draft' );
+ $response = json_decode( $this->_last_response, true );
+ $this->assertFalse( $response['success'] );
+ $this->assertEquals( 'missing_post_title', $response['data'] );
+ }
}
diff --git a/tests/phpunit/tests/customize/nav-menus.php b/tests/phpunit/tests/customize/nav-menus.php
index f5d56b661c..06e2333be3 100644
--- a/tests/phpunit/tests/customize/nav-menus.php
+++ b/tests/phpunit/tests/customize/nav-menus.php
@@ -45,9 +45,10 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
*/
function filter_item_types( $items ) {
$items[] = array(
- 'title' => 'Custom',
- 'type' => 'custom_type',
+ 'title' => 'Custom',
+ 'type' => 'custom_type',
'object' => 'custom_object',
+ 'type_label' => 'Custom Type',
);
return $items;
@@ -84,6 +85,21 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
do_action( 'customize_register', $this->wp_customize );
$menus = new WP_Customize_Nav_Menus( $this->wp_customize );
$this->assertInstanceOf( 'WP_Customize_Manager', $menus->manager );
+
+ $this->assertEquals( 10, add_filter( 'customize_refresh_nonces', array( $menus, 'filter_nonces' ) ) );
+ $this->assertEquals( 10, add_action( 'wp_ajax_load-available-menu-items-customizer', array( $menus, 'ajax_load_available_items' ) ) );
+ $this->assertEquals( 10, add_action( 'wp_ajax_search-available-menu-items-customizer', array( $menus, 'ajax_search_available_items' ) ) );
+ $this->assertEquals( 10, add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $menus, 'ajax_insert_auto_draft_post' ) ) );
+ $this->assertEquals( 10, add_action( 'customize_controls_enqueue_scripts', array( $menus, 'enqueue_scripts' ) ) );
+ $this->assertEquals( 11, add_action( 'customize_register', array( $menus, 'customize_register' ) ) );
+ $this->assertEquals( 10, add_filter( 'customize_dynamic_setting_args', array( $menus, 'filter_dynamic_setting_args' ) ) );
+ $this->assertEquals( 10, add_filter( 'customize_dynamic_setting_class', array( $menus, 'filter_dynamic_setting_class' ) ) );
+ $this->assertEquals( 10, add_action( 'customize_controls_print_footer_scripts', array( $menus, 'print_templates' ) ) );
+ $this->assertEquals( 10, add_action( 'customize_controls_print_footer_scripts', array( $menus, 'available_items_template' ) ) );
+ $this->assertEquals( 10, add_action( 'customize_preview_init', array( $menus, 'customize_preview_init' ) ) );
+ $this->assertEquals( 10, add_action( 'customize_preview_init', array( $menus, 'make_auto_draft_status_previewable' ) ) );
+ $this->assertEquals( 10, add_action( 'customize_save_nav_menus_created_posts', array( $menus, 'save_nav_menus_created_posts' ) ) );
+ $this->assertEquals( 10, add_filter( 'customize_dynamic_partial_args', array( $menus, 'customize_dynamic_partial_args' ) ) );
}
/**
@@ -444,10 +460,16 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
'menu-item-title' => 'Hello World',
'menu-item-status' => 'publish',
) );
- $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, "nav_menu_item[$item_id]" );
do_action( 'customize_register', $this->wp_customize );
+ $this->assertInstanceOf( 'WP_Customize_Nav_Menu_Item_Setting', $this->wp_customize->get_setting( "nav_menu_item[$item_id]" ) );
$this->assertEquals( 'Primary', $this->wp_customize->get_section( "nav_menu[$menu_id]" )->title );
$this->assertEquals( 'Hello World', $this->wp_customize->get_control( "nav_menu_item[$item_id]" )->label );
+
+ $nav_menus_created_posts_setting = $this->wp_customize->get_setting( 'nav_menus_created_posts' );
+ $this->assertInstanceOf( 'WP_Customize_Filter_Setting', $nav_menus_created_posts_setting );
+ $this->assertEquals( 'postMessage', $nav_menus_created_posts_setting->transport );
+ $this->assertEquals( array(), $nav_menus_created_posts_setting->default );
+ $this->assertEquals( array( $this->wp_customize->nav_menus, 'sanitize_nav_menus_created_posts' ), $nav_menus_created_posts_setting->sanitize_callback );
}
/**
@@ -479,24 +501,24 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
$menus = new WP_Customize_Nav_Menus( $this->wp_customize );
$expected = array(
- array( 'title' => 'Posts', 'type' => 'post_type', 'object' => 'post' ),
- array( 'title' => 'Pages', 'type' => 'post_type', 'object' => 'page' ),
- array( 'title' => 'Categories', 'type' => 'taxonomy', 'object' => 'category' ),
- array( 'title' => 'Tags', 'type' => 'taxonomy', 'object' => 'post_tag' ),
+ array( 'title' => 'Posts', 'type' => 'post_type', 'object' => 'post', 'type_label' => __( 'Post' ) ),
+ array( 'title' => 'Pages', 'type' => 'post_type', 'object' => 'page', 'type_label' => __( 'Page' ) ),
+ array( 'title' => 'Categories', 'type' => 'taxonomy', 'object' => 'category', 'type_label' => __( 'Category' ) ),
+ array( 'title' => 'Tags', 'type' => 'taxonomy', 'object' => 'post_tag', 'type_label' => __( 'Tag' ) ),
);
if ( current_theme_supports( 'post-formats' ) ) {
- $expected[] = array( 'title' => 'Format', 'type' => 'taxonomy', 'object' => 'post_format' );
+ $expected[] = array( 'title' => 'Format', 'type' => 'taxonomy', 'object' => 'post_format', 'type_label' => __( 'Format' ) );
}
$this->assertEquals( $expected, $menus->available_item_types() );
register_taxonomy( 'wptests_tax', array( 'post' ), array( 'labels' => array( 'name' => 'Foo' ) ) );
- $expected[] = array( 'title' => 'Foo', 'type' => 'taxonomy', 'object' => 'wptests_tax' );
+ $expected[] = array( 'title' => 'Foo', 'type' => 'taxonomy', 'object' => 'wptests_tax', 'type_label' => 'Foo' );
$this->assertEquals( $expected, $menus->available_item_types() );
- $expected[] = array( 'title' => 'Custom', 'type' => 'custom_type', 'object' => 'custom_object' );
+ $expected[] = array( 'title' => 'Custom', 'type' => 'custom_type', 'object' => 'custom_object', 'type_label' => 'Custom Type' );
add_filter( 'customize_nav_menu_available_item_types', array( $this, 'filter_item_types' ) );
$this->assertEquals( $expected, $menus->available_item_types() );
@@ -504,6 +526,29 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
}
+ /**
+ * Test insert_auto_draft_post method.
+ *
+ * @covers WP_Customize_Nav_Menus::insert_auto_draft_post()
+ */
+ public function test_insert_auto_draft_post() {
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $r = $menus->insert_auto_draft_post( array() );
+ $this->assertInstanceOf( 'WP_Error', $r );
+ $this->assertEquals( 'unknown_post_type', $r->get_error_code() );
+
+ $r = $menus->insert_auto_draft_post( array( 'post_type' => 'fake' ) );
+ $this->assertInstanceOf( 'WP_Error', $r );
+ $this->assertEquals( 'unknown_post_type', $r->get_error_code() );
+
+ $r = $menus->insert_auto_draft_post( array( 'post_title' => 'Hello World', 'post_type' => 'post' ) );
+ $this->assertInstanceOf( 'WP_Post', $r );
+ $this->assertEquals( 'Hello World', $r->post_title );
+ $this->assertEquals( 'post', $r->post_type );
+ $this->assertEquals( sanitize_title( $r->post_title ), $r->post_name );
+ }
+
/**
* Test the print_templates method.
*
@@ -553,6 +598,7 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
$this->assertRegExp( '#\s*' . esc_html( $type->labels->name ) . '#', $template );
$this->assertContains( 'data-type="post_type"', $template );
$this->assertContains( 'data-object="' . esc_attr( $type->name ) . '"', $template );
+ $this->assertContains( 'data-type_label="' . esc_attr( $type->labels->singular_name ) . '"', $template );
}
}
@@ -563,6 +609,7 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
$this->assertRegExp( '#\s*' . esc_html( $tax->labels->name ) . '#', $template );
$this->assertContains( 'data-type="taxonomy"', $template );
$this->assertContains( 'data-object="' . esc_attr( $tax->name ) . '"', $template );
+ $this->assertContains( 'data-type_label="' . esc_attr( $tax->labels->singular_name ) . '"', $template );
}
}
@@ -570,6 +617,7 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
$this->assertRegExp( '#\s*Custom#', $template );
$this->assertContains( 'data-type="custom_type"', $template );
$this->assertContains( 'data-object="custom_object"', $template );
+ $this->assertContains( 'data-type_label="Custom Type"', $template );
}
/**
@@ -609,6 +657,101 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
$this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
}
+ /**
+ * Test make_auto_draft_status_previewable.
+ *
+ * @covers WP_Customize_Nav_Menus::make_auto_draft_status_previewable()
+ */
+ function test_make_auto_draft_status_previewable() {
+ global $wp_post_statuses;
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+ $menus->make_auto_draft_status_previewable();
+ $this->assertTrue( $wp_post_statuses['auto-draft']->protected );
+ }
+
+ /**
+ * Test sanitize_nav_menus_created_posts.
+ *
+ * @covers WP_Customize_Nav_Menus::sanitize_nav_menus_created_posts()
+ */
+ function test_sanitize_nav_menus_created_posts() {
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+ $contributor_user_id = $this->factory()->user->create( array( 'role' => 'contributor' ) );
+ $author_user_id = $this->factory()->user->create( array( 'role' => 'author' ) );
+ $administrator_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
+
+ $contributor_post_id = $this->factory()->post->create( array(
+ 'post_status' => 'auto-draft',
+ 'post_title' => 'Contributor Post',
+ 'post_type' => 'post',
+ 'post_author' => $contributor_user_id,
+ ) );
+ $author_post_id = $this->factory()->post->create( array(
+ 'post_status' => 'auto-draft',
+ 'post_title' => 'Author Post',
+ 'post_type' => 'post',
+ 'post_author' => $author_user_id,
+ ) );
+ $administrator_post_id = $this->factory()->post->create( array(
+ 'post_status' => 'auto-draft',
+ 'post_title' => 'Admin Post',
+ 'post_type' => 'post',
+ 'post_author' => $administrator_user_id,
+ ) );
+
+ $value = array(
+ 'bad',
+ $contributor_post_id,
+ $author_post_id,
+ $administrator_post_id,
+ );
+
+ wp_set_current_user( $contributor_user_id );
+ $sanitized = $menus->sanitize_nav_menus_created_posts( $value );
+ $this->assertEquals( array(), $sanitized );
+
+ wp_set_current_user( $author_user_id );
+ $sanitized = $menus->sanitize_nav_menus_created_posts( $value );
+ $this->assertEquals( array( $author_post_id ), $sanitized );
+
+ wp_set_current_user( $administrator_user_id );
+ $sanitized = $menus->sanitize_nav_menus_created_posts( $value );
+ $this->assertEquals( array( $contributor_post_id, $author_post_id, $administrator_post_id ), $sanitized );
+ }
+
+ /**
+ * Test save_nav_menus_created_posts.
+ *
+ * @covers WP_Customize_Nav_Menus::save_nav_menus_created_posts()
+ */
+ function test_save_nav_menus_created_posts() {
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+ do_action( 'customize_register', $this->wp_customize );
+
+ $post_ids = $this->factory()->post->create_many( 3, array(
+ 'post_status' => 'auto-draft',
+ 'post_type' => 'post',
+ ) );
+ $pre_published_post_id = $this->factory()->post->create( array( 'post_status' => 'publish' ) );
+
+ $setting_id = 'nav_menus_created_posts';
+ $this->wp_customize->set_post_value( $setting_id, array_merge( $post_ids, array( $pre_published_post_id ) ) );
+ $setting = $this->wp_customize->get_setting( $setting_id );
+ $this->assertInstanceOf( 'WP_Customize_Filter_Setting', $setting );
+ $this->assertEquals( array( $menus, 'sanitize_nav_menus_created_posts' ), $setting->sanitize_callback );
+ $this->assertEquals( $post_ids, $setting->post_value() );
+ foreach ( $post_ids as $post_id ) {
+ $this->assertEquals( 'auto-draft', get_post_status( $post_id ) );
+ }
+
+ $save_action_count = did_action( 'customize_save_nav_menus_created_posts' );
+ $setting->save();
+ $this->assertEquals( $save_action_count + 1, did_action( 'customize_save_nav_menus_created_posts' ) );
+ foreach ( $post_ids as $post_id ) {
+ $this->assertEquals( 'publish', get_post_status( $post_id ) );
+ }
+ }
+
/**
* Test the filter_wp_nav_menu_args method.
*