Make WordPress Core

Opened 4 weeks ago

Last modified 4 weeks ago

#65671 new defect (bug)

is_page() and is_single() do not match a path that begins with a slash

Reported by: thisismyurl Owned by:
Priority: normal Milestone: Awaiting Review
Component: Query Version:
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses:

Description

WP_Query::is_page() and WP_Query::is_single() document their parameter as accepting a path:

@param int|string|int[]|string[] $page Optional. Page ID, title, slug, path, or array of such

Both guard the path branch with:

if ( ! strpos( $pagepath, '/' ) ) {
        continue;
}

strpos() returns 0 when the slash is the first character, so ! strpos() evaluates to true for a path such as /foo/bar, and the path is skipped without ever being checked.

get_page_by_path(), which performs the actual lookup, explicitly trims leading and trailing slashes:

$parts = explode( '/', trim( $page_path, '/' ) );

So a leading-slash path is input the lookup already supports. The guard is inconsistent with the function it guards, and the result is that is_page( '/foo/bar' ) returns false on the very page it names, with no notice or warning.

Steps to reproduce

  1. Create a page foo with a child page bar.
  2. Visit the child page.
  3. is_page( 'foo/bar' ) returns true.
  4. is_page( '/foo/bar' ) returns false.

is_single() has the same guard at the equivalent spot, so a post path behaves the same way.

Proposed fix

Use str_contains() instead of ! strpos(). It is polyfilled in core for PHP < 8.0 and is already used elsewhere in class-wp-query.php.

Behaviour is identical for every other input. Only a path whose first character is a slash changes, from silently skipped to correctly checked:

Input Current With fix
about skipped skipped
foo/bar checked checked
foo/bar/ checked checked
/foo/bar skipped checked

I have a patch and a regression test ready, and will open a PR once this ticket has a number.

Change History (1)

This ticket was mentioned in PR #12614 on WordPress/wordpress-develop by @thisismyurl.


4 weeks ago
#1

WP_Query::is_page() and WP_Query::is_single() document their parameter as accepting a path, and guard the path branch with:

if ( ! strpos( $pagepath, '/' ) ) {

continue;

}

strpos() returns 0 when the slash is the first character, so ! strpos() is true for a path like /foo/bar and the path is skipped without ever being checked. get_page_by_path(), which does the actual lookup, trims leading and trailing slashes, so that is input it already accepts. The result is that is_page( '/foo/bar' ) returns false on the very page it names, silently.

This switches both guards to str_contains(), which is polyfilled in core for PHP < 8.0 and already used elsewhere in class-wp-query.php.

Behaviour is identical for every other input. The affected set is any path whose first character is a slash, which is worth stating plainly because it is broader than the nested case that prompted the ticket: a single-segment path like /about also starts matching, since get_page_by_path() trims it to about and resolves it correctly.

Input Before After
about skipped skipped
foo/bar checked checked
foo/bar/ checked checked
/foo/bar skipped checked
/about skipped checked

The degenerate inputs / and // now reach get_page_by_path(), which trims them to an empty string and finds nothing. The result is still false, at the cost of one query that previously short-circuited. I mention it for completeness rather than because it changes anything observable.

Two regression tests sit beside the existing test_is_page_with_parent() and test_is_single_with_parent() and reuse their fixtures, one per changed line, each asserting both the slashless and leading-slash forms. Both fail on trunk and pass with the change.

One disclosure on verification: this box has no Docker/wp-env, so I could not run the full PHPUnit suite locally. I confirmed the behaviour difference with a standalone harness across the inputs in the table above, checked that get_page_by_path() trims the leading slash and that nothing upstream normalises it first, and ran php -l plus PHPCS (WordPress-Core, 0 errors on both changed files, no new warnings on the changed lines). I would appreciate a CI run confirming the tests.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Helping locate the faulty guard and cross-check it against get_page_by_path() and the documented parameter. I reviewed the change and take responsibility for the final code.

Note: See TracTickets for help on using tickets.