diff --git a/src/wp-includes/class-wp.php b/src/wp-includes/class-wp.php index efe77ad550..8a2f177a04 100644 --- a/src/wp-includes/class-wp.php +++ b/src/wp-includes/class-wp.php @@ -308,6 +308,9 @@ class WP { } } + // Resolve conflicts between posts with numeric slugs and date archive queries. + $this->query_vars = wp_resolve_numeric_slug_conflicts( $this->query_vars ); + foreach ( (array) $this->private_query_vars as $var) { if ( isset($this->extra_query_vars[$var]) ) $this->query_vars[$var] = $this->extra_query_vars[$var]; diff --git a/src/wp-includes/rewrite.php b/src/wp-includes/rewrite.php index dead05f741..3740c14cff 100644 --- a/src/wp-includes/rewrite.php +++ b/src/wp-includes/rewrite.php @@ -283,6 +283,116 @@ function _wp_filter_taxonomy_base( $base ) { return $base; } + +/** + * Resolve numeric slugs that collide with date permalinks. + * + * Permalinks of posts with numeric slugs can sometimes look to `WP_Query::parse_query()` like a date archive, + * as when your permalink structure is `/%year%/%postname%/` and a post with post_name '05' has the URL + * `/2015/05/`. This function detects conflicts of this type and resolves them in favor of the post permalink. + * + * Note that, since 4.3.0, `wp_unique_post_slug()` prevents the creation of post slugs that would result in a date + * archive conflict. The resolution performed in this function is primarily for legacy content, as well as cases when + * the admin has changed the site's permalink structure in a way that introduces URL conflicts. + * + * @since 4.3.0 + * + * @param array $query_vars Query variables for setting up the loop, as determined in `WP::parse_request()`. + * @return array Returns the original array of query vars, with date/post conflicts resolved. + */ +function wp_resolve_numeric_slug_conflicts( $query_vars = array() ) { + if ( ! isset( $query_vars['year'] ) && ! isset( $query_vars['monthnum'] ) && ! isset( $query_vars['day'] ) ) { + return $query_vars; + } + + // Identify the 'postname' position in the permastruct array. + $permastructs = array_values( array_filter( explode( '/', get_option( 'permalink_structure' ) ) ) ); + $postname_index = array_search( '%postname%', $permastructs ); + + if ( false === $postname_index ) { + return $query_vars; + } + + /* + * A numeric slug could be confused with a year, month, or day, depending on position. To account for + * the possibility of post pagination (eg 2015/2 for the second page of a post called '2015'), our + * `is_*` checks are generous: check for year-slug clashes when `is_year` *or* `is_month`, and check + * for month-slug clashes when `is_month` *or* `is_day`. + */ + $compare = ''; + if ( 0 === $postname_index && ( isset( $query_vars['year'] ) || isset( $query_vars['monthnum'] ) ) ) { + $compare = 'year'; + } elseif ( '%year%' === $permastructs[ $postname_index - 1 ] && ( isset( $query_vars['monthnum'] ) || isset( $query_vars['day'] ) ) ) { + $compare = 'monthnum'; + } elseif ( '%monthnum%' === $permastructs[ $postname_index - 1 ] && isset( $query_vars['day'] ) ) { + $compare = 'day'; + } + + if ( ! $compare ) { + return $query_vars; + } + + // This is the potentially clashing slug. + $value = $query_vars[ $compare ]; + + $post = get_page_by_path( $value, OBJECT, 'post' ); + if ( ! ( $post instanceof WP_Post ) ) { + return $query_vars; + } + + // If the date of the post doesn't match the date specified in the URL, resolve to the date archive. + if ( preg_match( '/^([0-9]{4})\-([0-9]{2})/', $post->post_date, $matches ) && isset( $query_vars['year'] ) && ( 'monthnum' === $compare || 'day' === $compare ) ) { + // $matches[1] is the year the post was published. + if ( intval( $query_vars['year'] ) !== intval( $matches[1] ) ) { + return $query_vars; + } + + // $matches[2] is the month the post was published. + if ( 'day' === $compare && isset( $query_vars['monthnum'] ) && intval( $query_vars['monthnum'] ) !== intval( $matches[2] ) ) { + return $query_vars; + } + } + + /* + * If the located post contains nextpage pagination, then the URL chunk following postname may be + * intended as the page number. Verify that it's a valid page before resolving to it. + */ + $maybe_page = ''; + if ( 'year' === $compare && isset( $query_vars['monthnum'] ) ) { + $maybe_page = $query_vars['monthnum']; + } elseif ( 'monthnum' === $compare && isset( $query_vars['day'] ) ) { + $maybe_page = $query_vars['day']; + } + + $post_page_count = substr_count( $post->post_content, '' ) + 1; + + // If the post doesn't have multiple pages, but a 'page' candidate is found, resolve to the date archive. + if ( 1 === $post_page_count && $maybe_page ) { + return $query_vars; + } + + // If the post has multiple pages and the 'page' number isn't valid, resolve to the date archive. + if ( $post_page_count > 1 && $maybe_page > $post_page_count ) { + return $query_vars; + } + + // If we've gotten to this point, we have a slug/date clash. First, adjust for nextpage. + if ( '' !== $maybe_page ) { + $query_vars['page'] = intval( $maybe_page ); + } + + // Next, unset autodetected date-related query vars. + unset( $query_vars['year'] ); + unset( $query_vars['monthnum'] ); + unset( $query_vars['day'] ); + + // Then, set the identified post. + $query_vars['name'] = $post->post_name; + + // Finally, return the modified query vars. + return $query_vars; +} + /** * Examine a url and try to determine the post ID it represents. * @@ -401,6 +511,9 @@ function url_to_postid( $url ) { } } + // Resolve conflicts between posts with numeric slugs and date archive queries. + $query = wp_resolve_numeric_slug_conflicts( $query ); + // Do the query $query = new WP_Query( $query ); if ( ! empty( $query->posts ) && $query->is_singular ) diff --git a/tests/phpunit/tests/rewrite/numericSlugs.php b/tests/phpunit/tests/rewrite/numericSlugs.php new file mode 100644 index 0000000000..769c65c482 --- /dev/null +++ b/tests/phpunit/tests/rewrite/numericSlugs.php @@ -0,0 +1,538 @@ +author_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + + // Override the post/archive slug collision prevention in `wp_unique_post_slug()`. + add_filter( 'wp_unique_post_slug', array( $this, 'filter_unique_post_slug' ), 10, 6 ); + } + + public function tearDown() { + remove_filter( 'wp_unique_post_slug', array( $this, 'filter_unique_post_slug' ), 10, 6 ); + } + + public function test_go_to_year_segment_collision_without_title() { + global $wp_rewrite, $wpdb; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '2015', + 'post_date' => '2015-02-01 01:00:00' + ) ); + + // Force an ID that resembles a year format + $wpdb->update( + $wpdb->posts, + array( + 'ID' => '2015', + 'guid' => 'http://example.org/?p=2015' + ), + array( 'ID' => $id ) + ); + + $this->go_to( get_permalink( '2015' ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_year_segment_collision_without_title() { + global $wp_rewrite, $wpdb; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '2015', + 'post_date' => '2015-02-01 01:00:00' + ) ); + + // Force an ID that resembles a year format + $wpdb->update( + $wpdb->posts, + array( + 'ID' => '2015', + 'guid' => 'http://example.org/?p=2015' + ), + array( 'ID' => $id ) + ); + + $this->assertEquals( '2015', url_to_postid( get_permalink( '2015' ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_year_segment_collision_with_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '2015', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_year_segment_collision_with_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '2015', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_month_segment_collision_without_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '02', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_month_segment_collision_without_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '02', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_month_segment_collision_without_title_no_leading_zero() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '2', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_month_segment_collision_without_title_no_leading_zero() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '2', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_month_segment_collision_with_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '02', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_month_segment_collision_with_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '02', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_month_segment_collision_with_title_no_leading_zero() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '2', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_month_segment_collision_with_title_no_leading_zero() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '2', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_day_segment_collision_without_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '01', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_day_segment_collision_without_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '', + 'post_name' => '01', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_go_to_day_segment_collision_with_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '01', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_url_to_postid_day_segment_collision_with_title() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '01', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->assertEquals( $id, url_to_postid( get_permalink( $id ) ) ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_numeric_slug_permalink_conflicts_should_only_be_resolved_for_the_main_query() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => '01', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $q = new WP_Query( array( + 'year' => '2015', + 'monthnum' => '02', + 'day' => '01', + ) ); + + $this->assertTrue( $q->is_day ); + $this->assertFalse( $q->is_single ); + + $wp_rewrite->set_permalink_structure(''); + } + + public function test_month_slug_collision_should_resolve_to_date_archive_when_year_does_not_match_post_year() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + // Make sure a post is published in 2013/02, to avoid 404s. + $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'foo', + 'post_title' => 'bar', + 'post_date' => '2013-02-01 01:00:00', + ) ); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'foo', + 'post_title' => '02', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $permalink = get_permalink( $id ); + $permalink = str_replace( '/2015/', '/2013/', $permalink ); + + $this->go_to( $permalink ); + + $this->assertTrue( is_month() ); + } + + public function test_day_slug_collision_should_resolve_to_date_archive_when_monthnum_does_not_match_post_month() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + $wp_rewrite->flush_rules(); + + // Make sure a post is published on 2015/01/01, to avoid 404s. + $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'foo', + 'post_title' => 'bar', + 'post_date' => '2015-01-02 01:00:00', + ) ); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'foo', + 'post_title' => '02', + 'post_date' => '2015-02-02 01:00:00', + ) ); + + $permalink = get_permalink( $id ); + $permalink = str_replace( '/2015/02/', '/2015/01/', $permalink ); + + $this->go_to( $permalink ); + + $this->assertTrue( is_day() ); + } + + public function test_date_slug_collision_should_distinguish_valid_pagination_from_date() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'Page 0Page 1Page 2Page 3', + 'post_title' => '02', + 'post_date' => '2015-02-01 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) . '1' ); + + $this->assertFalse( is_day() ); + } + + public function test_date_slug_collision_should_distinguish_too_high_pagination_from_date() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'Page 0Page 1Page 2Page 3', + 'post_title' => '02', + 'post_date' => '2015-02-05 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) . '5' ); + + $this->assertTrue( is_day() ); + } + + public function test_date_slug_collision_should_not_require_pagination_query_var() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'Page 0Page 1Page 2Page 3', + 'post_title' => '02', + 'post_date' => '2015-02-05 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) ); + + $this->assertQueryTrue( 'is_single', 'is_singular' ); + $this->assertFalse( is_date() ); + } + + public function test_date_slug_collision_should_be_ignored_when_pagination_var_is_present_but_post_does_not_have_multiple_pages() { + global $wp_rewrite; + $wp_rewrite->init(); + $wp_rewrite->set_permalink_structure( '/%year%/%postname%/' ); + $wp_rewrite->flush_rules(); + + $id = $this->factory->post->create( array( + 'post_author' => $this->author_id, + 'post_status' => 'publish', + 'post_content' => 'This post does not have pagination.', + 'post_title' => '02', + 'post_date' => '2015-02-05 01:00:00', + ) ); + + $this->go_to( get_permalink( $id ) . '5' ); + + $this->assertTrue( is_day() ); + } + + public function filter_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_parent, $original_slug ) { + return $original_slug; + } +}