From 7105787ced1ea67b2663847e2f9b0d8c5b5b91ab Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Aug 2016 22:58:32 +0000 Subject: [PATCH] Customize: Allow users to more seamlessly create page-based nav menus during customization. Introduces the ability to create stubs for the various post types to add to a given menu. This eliminates the need to leave the customizer to first create the post in the admin and then return to managing menus. Only the title of the newly-created post can be supplied; the post content will be blank and will need to be provided in the normal edit post screen outside the customizer, unless a plugin enables a post editing in the customizer experience. When a post is created and added to a nav menu in the customizer, the newly created post that is added to a menu is given the `auto-draft` status, and if the changes are not published, the `auto-draft` post will be automatically deleted within 7 days via `wp_delete_auto_drafts()`. However, if the customizer changes are saved, then these nav menu item `auto-draft` post stubs will be transitioned to `publish`. Includes portions of code from the Customize Posts and Front-end Editor plugins. For more information, see https://make.wordpress.org/core/2016/06/16/feature-proposal-content-authorship-in-menus-with-live-preview/ Props celloexpressions, westonruter, valendesigns, afercia, melchoyce, mapk, iseulde, mrahmadawais. Fixes #34923. git-svn-id: https://develop.svn.wordpress.org/trunk@38436 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/customize-controls.css | 3 +- src/wp-admin/css/customize-nav-menus.css | 63 ++++- src/wp-admin/js/customize-nav-menus.js | 141 ++++++++++- .../class-wp-customize-nav-menus.php | 219 +++++++++++++++++- .../class-wp-customize-setting.php | 2 + tests/phpunit/tests/ajax/CustomizeMenus.php | 110 +++++++++ tests/phpunit/tests/customize/nav-menus.php | 163 ++++++++++++- 7 files changed, 672 insertions(+), 29 deletions(-) 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. *