Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/wp-includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 );
}

/**
Expand Down
85 changes: 76 additions & 9 deletions tests/phpunit/tests/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 ),
);
}

Expand Down
Loading