diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 9315ba7fb7ff9..8b3027bf7de2e 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -346,6 +346,28 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $tax_url = get_term_link( (int) $obj->term_id, $obj->taxonomy ); if ( $tax_url && ! is_wp_error( $tax_url ) ) { + $category_permastruct = $wp_rewrite->get_category_permastruct(); + + // Ensure the correct parent-child category path is used in permalink. + if ( is_category() + && ! empty( $obj->parent ) + && $category_permastruct + && str_contains( $category_permastruct, '%category%' ) + && ! empty( $wp_query->query['category_name'] ) + ) { + $category_path_slug = get_category_parents( $obj->term_id, false, '/', true ); + + if ( $category_path_slug && ! is_wp_error( $category_path_slug ) ) { + $category_path_slug = untrailingslashit( $category_path_slug ); + $category_path = str_replace( '%category%', $category_path_slug, $category_permastruct ); + $request_category_path = str_replace( '%category%', $wp_query->query['category_name'], $category_permastruct ); + + if ( $category_path !== $request_category_path ) { + $redirect['path'] = str_replace( $request_category_path, $category_path, $redirect['path'] ); + } + } + } + if ( ! empty( $redirect['query'] ) ) { // Strip taxonomy query vars off the URL. $qv_remove = array( 'term', 'taxonomy' ); diff --git a/tests/phpunit/includes/testcase-canonical.php b/tests/phpunit/includes/testcase-canonical.php index 33dbb3d9f438b..c98f7c15578e5 100644 --- a/tests/phpunit/includes/testcase-canonical.php +++ b/tests/phpunit/includes/testcase-canonical.php @@ -259,6 +259,33 @@ public static function generate_shared_fixtures( WP_UnitTest_Factory $factory ) ) ); + // Insert a few posts in each category to enable pagination. + for ( $p = 0; $p < 6; $p++ ) { + self::$post_ids[] = $factory->post->create( + array( + 'post_title' => 'Post in category parent ' . $p, + 'post_type' => 'post', + 'post_category' => array( self::$terms['/category/parent/'] ), + ) + ); + + self::$post_ids[] = $factory->post->create( + array( + 'post_title' => 'Post in category child-1 ' . $p, + 'post_type' => 'post', + 'post_category' => array( self::$terms['/category/parent/child-1/'] ), + ) + ); + + self::$post_ids[] = $factory->post->create( + array( + 'post_title' => 'Post in category child-2 ' . $p, + 'post_type' => 'post', + 'post_category' => array( self::$terms['/category/parent/child-1/child-2/'] ), + ) + ); + } + self::$term_ids[ $tag1 ] = 'post_tag'; } diff --git a/tests/phpunit/tests/canonical.php b/tests/phpunit/tests/canonical.php index 886b09312910e..37848f589d701 100644 --- a/tests/phpunit/tests/canonical.php +++ b/tests/phpunit/tests/canonical.php @@ -126,6 +126,56 @@ public function data_canonical() { 17174, ), + // Child categories with missing parent category slugs in the URL. + array( + '/category/child-1/', + array( + 'url' => '/category/parent/child-1/', + 'qv' => array( 'category_name' => 'parent/child-1' ), + ), + ), + array( + '/category/child-2/', + array( + 'url' => '/category/parent/child-1/child-2/', + 'qv' => array( 'category_name' => 'parent/child-1/child-2' ), + ), + ), + array( + '/category/parent/child-2/', + array( + 'url' => '/category/parent/child-1/child-2/', + 'qv' => array( 'category_name' => 'parent/child-1/child-2' ), + ), + ), + array( + '/category/too/many/parents/child-1/', + array( + 'url' => '/category/parent/child-1/', + 'qv' => array( 'category_name' => 'parent/child-1' ), + ), + ), + array( + '/category/child-1/page/2/', + array( + 'url' => '/category/parent/child-1/page/2/', + 'qv' => array( + 'category_name' => 'parent/child-1', + 'paged' => 2, + ), + ), + ), + array( + '/category/child-1/child-2/page/2/', + array( + 'url' => '/category/parent/child-1/child-2/page/2/', + 'qv' => array( + 'category_name' => 'parent/child-1/child-2', + 'paged' => 2, + ), + ), + ), + // Categories & intersections with other vars. array( '/category/uncategorized/?tag=post-formats',