diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 908b80c333..adb0dd9cd6 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -327,46 +327,11 @@ function edit_post( $post_data = null ) { // Convert taxonomy input to term IDs, to avoid ambiguity. if ( isset( $post_data['tax_input'] ) ) { foreach ( (array) $post_data['tax_input'] as $taxonomy => $terms ) { - // Hierarchical taxonomy data is already sent as term IDs, so no conversion is necessary. - if ( is_taxonomy_hierarchical( $taxonomy ) ) { - continue; + $tax_object = get_taxonomy( $taxonomy ); + + if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) { + $post_data['tax_input'][ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) ); } - - /* - * Assume that a 'tax_input' string is a comma-separated list of term names. - * Some languages may use a character other than a comma as a delimiter, so we standardize on - * commas before parsing the list. - */ - if ( ! is_array( $terms ) ) { - $comma = _x( ',', 'tag delimiter' ); - if ( ',' !== $comma ) { - $terms = str_replace( $comma, ',', $terms ); - } - $terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) ); - } - - $clean_terms = array(); - foreach ( $terms as $term ) { - // Empty terms are invalid input. - if ( empty( $term ) ) { - continue; - } - - $_term = get_terms( $taxonomy, array( - 'name' => $term, - 'fields' => 'ids', - 'hide_empty' => false, - ) ); - - if ( ! empty( $_term ) ) { - $clean_terms[] = intval( $_term[0] ); - } else { - // No existing term was found, so pass the string. A new term will be created. - $clean_terms[] = $term; - } - } - - $post_data['tax_input'][ $taxonomy ] = $clean_terms; } } @@ -1870,3 +1835,61 @@ function redirect_post($post_id = '') { wp_redirect( apply_filters( 'redirect_post_location', $location, $post_id ) ); exit; } + +/** + * Sanitizes POST values from a checkbox taxonomy metabox. + * + * @since 5.0.0 + * + * @param mixed $terms Raw term data from the 'tax_input' field. + * @return array + */ +function taxonomy_meta_box_sanitize_cb_checkboxes( $taxonmy, $terms ) { + return array_map( 'intval', $terms ); +} + +/** + * Sanitizes POST values from an input taxonomy metabox. + * + * @since 5.0.0 + * + * @param mixed $terms Raw term data from the 'tax_input' field. + * @return array + */ +function taxonomy_meta_box_sanitize_cb_input( $taxonomy, $terms ) { + /* + * Assume that a 'tax_input' string is a comma-separated list of term names. + * Some languages may use a character other than a comma as a delimiter, so we standardize on + * commas before parsing the list. + */ + if ( ! is_array( $terms ) ) { + $comma = _x( ',', 'tag delimiter' ); + if ( ',' !== $comma ) { + $terms = str_replace( $comma, ',', $terms ); + } + $terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) ); + } + + $clean_terms = array(); + foreach ( $terms as $term ) { + // Empty terms are invalid input. + if ( empty( $term ) ) { + continue; + } + + $_term = get_terms( $taxonomy, array( + 'name' => $term, + 'fields' => 'ids', + 'hide_empty' => false, + ) ); + + if ( ! empty( $_term ) ) { + $clean_terms[] = intval( $_term[0] ); + } else { + // No existing term was found, so pass the string. A new term will be created. + $clean_terms[] = $term; + } + } + + return $clean_terms; +} diff --git a/src/wp-includes/class-wp-taxonomy.php b/src/wp-includes/class-wp-taxonomy.php index e749eef2d1..3a92ba859e 100644 --- a/src/wp-includes/class-wp-taxonomy.php +++ b/src/wp-includes/class-wp-taxonomy.php @@ -127,6 +127,15 @@ final class WP_Taxonomy { */ public $meta_box_cb = null; + /** + * The callback function for sanitizing taxonomy data saved from a meta box. + * + * @since 5.0.0 + * @access public + * @var callable + */ + public $meta_box_sanitize_cb = null; + /** * An array of object types this taxonomy is registered for. * @@ -257,6 +266,7 @@ final class WP_Taxonomy { 'show_in_quick_edit' => null, 'show_admin_column' => false, 'meta_box_cb' => null, + 'meta_box_sanitize_cb' => null, 'capabilities' => array(), 'rewrite' => true, 'query_var' => $this->name, @@ -345,6 +355,20 @@ final class WP_Taxonomy { $args['name'] = $this->name; + // Default meta box sanitization callback depends on the value of 'meta_box_cb'. + if ( null === $args['meta_box_sanitize_cb'] ) { + switch ( $args['meta_box_cb'] ) { + case 'post_categories_meta_box' : + $args['meta_box_sanitize_cb'] = 'taxonomy_meta_box_sanitize_cb_checkboxes'; + break; + + case 'post_tags_meta_box' : + default : + $args['meta_box_sanitize_cb'] = 'taxonomy_meta_box_sanitize_cb_input'; + break; + } + } + foreach ( $args as $property_name => $property_value ) { $this->$property_name = $property_value; } diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 92a4ee73d2..bdbeb3dc9d 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -297,6 +297,7 @@ function is_taxonomy_hierarchical($taxonomy) { * @since 4.5.0 Introduced `publicly_queryable` argument. * @since 4.7.0 Introduced `show_in_rest`, 'rest_base' and 'rest_controller_class' * arguments to register the Taxonomy in REST API. + * @since 5.0.0 Introduced `meta_box_sanitize_cb` argument. * * @global array $wp_taxonomies Registered taxonomies. * @@ -339,6 +340,9 @@ function is_taxonomy_hierarchical($taxonomy) { * post_categories_meta_box() is used for hierarchical taxonomies, and * post_tags_meta_box() is used for non-hierarchical. If false, no meta * box is shown. + * @type callable $meta_box_sanitize_cb Callback function for sanitizing taxonomy data saved from a meta + * box. If no callback is defined, an appropriate one is determined + * based on the value of `$meta_box_cb`. * @type array $capabilities { * Array of capabilities for this taxonomy. * diff --git a/tests/phpunit/tests/taxonomy.php b/tests/phpunit/tests/taxonomy.php index b5fd536100..7d2a9aa5e1 100644 --- a/tests/phpunit/tests/taxonomy.php +++ b/tests/phpunit/tests/taxonomy.php @@ -791,4 +791,42 @@ class Tests_Taxonomy extends WP_UnitTestCase { $this->assertSame( 'foo', $taxonomy->name ); } + + /** + * @ticket 36514 + */ + public function test_edit_post_hierarchical_taxonomy() { + + $taxonomy_name = 'foo'; + $term_name = 'bar'; + + register_taxonomy( $taxonomy_name, array( 'post' ), array( + 'hierarchical' => false, + 'meta_box_cb' => 'post_categories_meta_box', + ) ); + $post = self::factory()->post->create_and_get( array( + 'post_type' => 'post', + ) ); + + $term_id = self::factory()->term->create_object( array( + 'name' => $term_name, + 'taxonomy' => $taxonomy_name, + ) ); + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) ); + $updated_post_id = edit_post( array( + 'post_ID' => $post->ID, + 'post_type' => 'post', + 'tax_input' => array( + $taxonomy_name => array( + (string) $term_id // Cast term_id as string to match whats sent in WP Admin. + ), + ), + ) ); + + $terms_obj = get_the_terms( $updated_post_id, $taxonomy_name ); + $problematic_term = current( wp_list_pluck( $terms_obj, 'name' ) ); + + $this->assertEquals( $problematic_term, $term_name ); + } }