From 2e2ea4876ba0fa247e57d6dfdc27e7d056a08d51 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 9 Nov 2015 00:47:55 +0000 Subject: [PATCH] Customize: Improve alignment of `WP_Customize_Nav_Menu_Item_Setting::sanitize()` behavior with `wp_update_nav_menu_item()`. * Apply `title_save_pre`, `excerpt_save_pre`, and `content_save_pre` filters on a nav menu item's `title`, `attr_title`, and `description` properties respectively. This ensures that arbitrary markup can be supplied if the user has `unfiltered_html` cap, and for these fields to have markup stripped if not. * Ensure a nav menu item's `post_status` is sanitized as `publish` or `draft` using the same conditions as `wp_update_nav_menu_item()`. * Align `WP_Customize_Nav_Menu_Item_Setting::sanitize()` behavior for sanitizing `position` to be the same as `wp_update_nav_menu_item()`. * Also apply `nav_menu_attr_title` and `nav_menu_description` filters in `WP_Customize_Nav_Menu_Item_Setting::value_as_wp_post_nav_menu_item()` to ensure that previewing markup entered into menu item description will preview the same way as when the nav menu item is saved. * Add unit tests. Fixes #32812. git-svn-id: https://develop.svn.wordpress.org/trunk@35580 602fd350-edb4-49c9-b593-d223f7449a82 --- ...ass-wp-customize-nav-menu-item-setting.php | 22 ++- .../tests/customize/nav-menu-item-setting.php | 128 +++++++++++++++--- 2 files changed, 124 insertions(+), 26 deletions(-) diff --git a/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php b/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php index 463da87afe..2fa0b5c0d2 100644 --- a/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php +++ b/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php @@ -572,6 +572,12 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } } + /** This filter is documented in wp-includes/nav-menu.php */ + $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title ); + + /** This filter is documented in wp-includes/nav-menu.php */ + $post->description = apply_filters( 'nav_menu_description', wp_trim_words( $post->description, 200 ) ); + return $post; } @@ -619,7 +625,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { ); $menu_item_value = array_merge( $default, $menu_item_value ); $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) ); - $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) ); + $menu_item_value['position'] = intval( $menu_item_value['position'] ); foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { // Note we need to allow negative-integer IDs for previewed objects not inserted yet. @@ -638,14 +644,16 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); } - foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) { - // @todo Should esc_attr() the attr_title as well? - $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] ); - } + $menu_item_value['original_title'] = sanitize_text_field( $menu_item_value['original_title'] ); + + // Apply the same filters as when calling wp_insert_post(). + $menu_item_value['title'] = apply_filters( 'title_save_pre', $menu_item_value['title'] ); + $menu_item_value['attr_title'] = apply_filters( 'excerpt_save_pre', $menu_item_value['attr_title'] ); + $menu_item_value['description'] = apply_filters( 'content_save_pre', $menu_item_value['description'] ); $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] ); - if ( ! get_post_status_object( $menu_item_value['status'] ) ) { - $menu_item_value['status'] = 'publish'; + if ( 'publish' !== $menu_item_value['status'] ) { + $menu_item_value['status'] = 'draft'; } $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid']; diff --git a/tests/phpunit/tests/customize/nav-menu-item-setting.php b/tests/phpunit/tests/customize/nav-menu-item-setting.php index 367d94744a..6b1a839518 100644 --- a/tests/phpunit/tests/customize/nav-menu-item-setting.php +++ b/tests/phpunit/tests/customize/nav-menu-item-setting.php @@ -438,6 +438,8 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { */ function test_sanitize() { do_action( 'customize_register', $this->wp_customize ); + + $menu_id = wp_create_nav_menu( 'Primary' ); $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' ); $this->assertNull( $setting->sanitize( 'not an array' ) ); @@ -449,37 +451,76 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { 'menu_item_parent' => 'asdasd', 'position' => -123, 'type' => 'custom', - 'title' => 'Hi', + 'title' => 'Hi', 'url' => 'javascript:alert(1)', 'target' => '" onclick="', - 'attr_title' => 'evil', - 'description' => 'Hello world', + 'attr_title' => 'bolded', + 'description' => 'Hello world', 'classes' => 'hello " inject="', 'xfn' => 'hello " inject="', 'status' => 'forbidden', - 'original_title' => 'Hi', + 'original_title' => 'Hi', 'nav_menu_term_id' => 'heilo', '_invalid' => false, ); + $expected_sanitized = array( + 'object_id' => 0, + 'object' => 'bhellob', + 'menu_item_parent' => 0, + 'position' => -123, + 'type' => 'customb', + 'title' => current_user_can( 'unfiltered_html' ) ? 'Hi' : 'HiunfilteredHtml()', + 'url' => '', + 'target' => 'onclick', + 'attr_title' => current_user_can( 'unfiltered_html' ) ? 'bolded' : 'boldedunfilteredHtml()', + 'description' => current_user_can( 'unfiltered_html' ) ? 'Hello world' : 'Hello worldunfilteredHtml()', + 'classes' => 'hello inject', + 'xfn' => 'hello inject', + 'status' => 'draft', + 'original_title' => 'Hi', + 'nav_menu_term_id' => 0, + ); + $sanitized = $setting->sanitize( $unsanitized ); $this->assertEqualSets( array_keys( $unsanitized ), array_keys( $sanitized ) ); - $this->assertEquals( 0, $sanitized['object_id'] ); - $this->assertEquals( 'bhellob', $sanitized['object'] ); - $this->assertEquals( 0, $sanitized['menu_item_parent'] ); - $this->assertEquals( 0, $sanitized['position'] ); - $this->assertEquals( 'customb', $sanitized['type'] ); - $this->assertEquals( 'Hi', $sanitized['title'] ); - $this->assertEquals( '', $sanitized['url'] ); - $this->assertEquals( 'onclick', $sanitized['target'] ); - $this->assertEquals( 'evil', $sanitized['attr_title'] ); - $this->assertEquals( 'Hello world', $sanitized['description'] ); - $this->assertEquals( 'hello inject', $sanitized['classes'] ); - $this->assertEquals( 'hello inject', $sanitized['xfn'] ); - $this->assertEquals( 'publish', $sanitized['status'] ); - $this->assertEquals( 'Hi', $sanitized['original_title'] ); - $this->assertEquals( 0, $sanitized['nav_menu_term_id'] ); + foreach ( $expected_sanitized as $key => $value ) { + $this->assertEquals( $value, $sanitized[ $key ], "Expected $key to be sanitized." ); + } + + $nav_menu_item_id = wp_update_nav_menu_item( $menu_id, 0, array( + 'menu-item-object-id' => $unsanitized['object_id'], + 'menu-item-object' => $unsanitized['object'], + 'menu-item-parent-id' => $unsanitized['menu_item_parent'], + 'menu-item-position' => $unsanitized['position'], + 'menu-item-type' => $unsanitized['type'], + 'menu-item-title' => $unsanitized['title'], + 'menu-item-url' => $unsanitized['url'], + 'menu-item-description' => $unsanitized['description'], + 'menu-item-attr-title' => $unsanitized['attr_title'], + 'menu-item-target' => $unsanitized['target'], + 'menu-item-classes' => $unsanitized['classes'], + 'menu-item-xfn' => $unsanitized['xfn'], + 'menu-item-status' => $unsanitized['status'], + ) ); + + $post = get_post( $nav_menu_item_id ); + $nav_menu_item = wp_setup_nav_menu_item( clone $post ); + + $this->assertEquals( $expected_sanitized['object_id'], $nav_menu_item->object_id ); + $this->assertEquals( $expected_sanitized['object'], $nav_menu_item->object ); + $this->assertEquals( $expected_sanitized['menu_item_parent'], $nav_menu_item->menu_item_parent ); + $this->assertEquals( $expected_sanitized['position'], $post->menu_order ); + $this->assertEquals( $expected_sanitized['type'], $nav_menu_item->type ); + $this->assertEquals( $expected_sanitized['title'], $post->post_title ); + $this->assertEquals( $expected_sanitized['url'], $nav_menu_item->url ); + $this->assertEquals( $expected_sanitized['description'], $post->post_content ); + $this->assertEquals( $expected_sanitized['attr_title'], $post->post_excerpt ); + $this->assertEquals( $expected_sanitized['target'], $nav_menu_item->target ); + $this->assertEquals( $expected_sanitized['classes'], implode( ' ', $nav_menu_item->classes ) ); + $this->assertEquals( $expected_sanitized['xfn'], $nav_menu_item->xfn ); + $this->assertEquals( $expected_sanitized['status'], $post->post_status ); } /** @@ -709,4 +750,53 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { $this->assertTrue( $value_object->_invalid ); } + /** + * Test WP_Customize_Nav_Menu_Item_Setting::value_as_wp_post_nav_menu_item(). + * + * @see WP_Customize_Nav_Menu_Item_Setting::value_as_wp_post_nav_menu_item() + */ + function test_value_as_wp_post_nav_menu_item() { + $post_id = self::factory()->post->create(); + + $setting = new WP_Customize_Nav_Menu_Item_Setting( + $this->wp_customize, + 'nav_menu_item[123]' + ); + $post_value = array( + 'object_id' => $post_id, + 'object' => 'post', + 'menu_item_parent' => 0, + 'position' => 2, + 'type' => 'post_type', + 'title' => 'Hello World', + 'url' => '', + 'target' => '', + 'attr_title' => '">attempted baddie', + 'description' => 'Attempted markup', + 'classes' => '', + 'xfn' => '', + 'status' => 'publish', + 'original_title' => '', + 'nav_menu_term_id' => 0, + '_invalid' => false, + ); + $this->wp_customize->set_post_value( $setting->id, $post_value ); + + $setting->preview(); + $nav_menu_item = $setting->value_as_wp_post_nav_menu_item(); + + $this->assertObjectNotHasAttribute( 'nav_menu_term_id', $nav_menu_item ); + $this->assertObjectNotHasAttribute( 'status', $nav_menu_item ); + $this->assertEquals( 'publish', $nav_menu_item->post_status ); + $this->assertEquals( 'nav_menu_item', $nav_menu_item->post_type ); + $this->assertObjectNotHasAttribute( 'position', $nav_menu_item ); + $this->assertEquals( $post_value['position'], $nav_menu_item->menu_order ); + $this->assertEquals( $post_value['title'], $nav_menu_item->post_title ); + $this->assertEquals( 123, $nav_menu_item->ID ); + $this->assertEquals( 123, $nav_menu_item->db_id ); + $this->assertEquals( wp_get_current_user()->ID, $nav_menu_item->post_author ); + $this->assertObjectHasAttribute( 'type_label', $nav_menu_item ); + $this->assertEquals( '“>attempted baddie', $nav_menu_item->attr_title ); + $this->assertEquals( 'Attempted markup', $nav_menu_item->description ); + } }