diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 3d9cedb7ecdda..14cc9546528e6 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -7543,9 +7543,10 @@ function _device_can_upload() { } /** - * Tests if a given path is a stream URL + * Tests if a given path is a stream URL. * * @since 3.5.0 + * @since 7.2.0 A lowercased scheme is accepted as a fallback, matching PHP. * * @param string $path The resource path or URL. * @return bool True if the path is a stream URL. @@ -7558,9 +7559,15 @@ function wp_is_stream( $path ) { return false; } - $stream = substr( $path, 0, $scheme_separator ); + $stream = substr( $path, 0, $scheme_separator ); + $wrappers = stream_get_wrappers(); - return in_array( $stream, stream_get_wrappers(), true ); + /* + * PHP looks the scheme up as given, then retries once with it lowercased. + * This is not a case-insensitive match: a wrapper registered as `MyStream` + * is not reachable as `mystream`. + */ + return in_array( $stream, $wrappers, true ) || in_array( strtolower( $stream ), $wrappers, true ); } /** diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index 608b31f058d99..5b5660df2439a 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -2121,6 +2121,8 @@ public function data_validate_file() { /** * Test stream URL validation. * + * @ticket 65870 + * * @dataProvider data_wp_is_stream * * @param string $path The resource path or URL. @@ -2147,17 +2149,82 @@ public function test_wp_is_stream( $path, $expected ) { public function data_wp_is_stream() { return array( // Legitimate stream examples. - array( 'http://example.com', true ), - array( 'https://example.com', true ), - array( 'ftp://example.com', true ), - array( 'file:///path/to/some/file', true ), - array( 'php://some/php/file.php', true ), + 'HTTP URL' => array( 'http://example.com', true ), + 'HTTPS URL' => array( 'https://example.com', true ), + 'FTP URL' => array( 'ftp://example.com', true ), + 'file URL' => array( 'file:///path/to/some/file', true ), + 'uppercase file scheme' => array( 'FILE:///path/to/some/file', true ), + 'PHP stream URL' => array( 'php://some/php/file.php', true ), // Non-stream examples. - array( 'fakestream://foo/bar/baz', false ), - array( '../../some/relative/path', false ), - array( 'some/other/relative/path', false ), - array( '/leading/relative/path', false ), + 'unregistered stream scheme' => array( 'fakestream://foo/bar/baz', false ), + 'parent-relative path' => array( '../../some/relative/path', false ), + 'relative path' => array( 'some/other/relative/path', false ), + 'absolute path' => array( '/leading/relative/path', false ), + ); + } + + /** + * Tests scheme case matching against PHP's wrapper selection. + * + * @ticket 65870 + * + * @dataProvider data_wp_is_stream_matches_php_case_matching + * + * @param string $scheme The registered scheme. + * @param string $path The path to open. + * @param bool $expected Whether PHP should select the registered wrapper. + */ + public function test_wp_is_stream_matches_php_case_matching( $scheme, $path, $expected ) { + $wrapper = new class() { + public $context; + public static $invoked = false; + + public function stream_open( $path, $mode, $options, &$opened_path ) { + self::$invoked = true; + return false; + } + }; + + $wrapper::$invoked = false; + $registered = false; + $handle = false; + + try { + $registered = stream_wrapper_register( $scheme, get_class( $wrapper ) ); + $this->assertTrue( $registered ); + + // Record selection without requiring the wrapper to parse or open the URL. + $handle = @fopen( $path, 'r' ); + + $this->assertSame( $expected, $wrapper::$invoked, 'PHP did not select the expected wrapper.' ); + $this->assertSame( $wrapper::$invoked, wp_is_stream( $path ), 'WordPress and PHP disagree on scheme case matching.' ); + } finally { + if ( is_resource( $handle ) ) { + fclose( $handle ); + } + if ( $registered ) { + stream_wrapper_unregister( $scheme ); + } + } + } + + /** + * Data provider for scheme case matching against PHP. + * + * @return array[] + */ + public function data_wp_is_stream_matches_php_case_matching() { + return array( + 'lowercase scheme' => array( 'wpteststream', 'wpteststream://bucket/file', true ), + 'lowercase registration mixed case' => array( 'wpteststream', 'wpTestStream://bucket/file', true ), + 'lowercase registration uppercase' => array( 'wpteststream', 'WPTESTSTREAM://bucket/file', true ), + 'mixed case exact match' => array( 'wpTestStream', 'wpTestStream://bucket/file', true ), + 'mixed case lowercased' => array( 'wpTestStream', 'wpteststream://bucket/file', false ), + 'mixed case uppercased' => array( 'wpTestStream', 'WPTESTSTREAM://bucket/file', false ), + 'mixed case with different casing' => array( 'wpTestStream', 'wptestSTREAM://bucket/file', false ), + 'uppercase exact match' => array( 'WPTESTSTREAM', 'WPTESTSTREAM://bucket/file', true ), + 'uppercase lowercased' => array( 'WPTESTSTREAM', 'wpteststream://bucket/file', false ), ); }