diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index 130eae3c37..3738b791cf 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -633,35 +633,68 @@ function ms_allowed_http_request_hosts( $is_external, $host ) { * when URL parsing failed. * * @since 4.4.0 + * @since 4.7.0 The $component parameter was added for parity with PHP's parse_url(). * - * @param string $url The URL to parse. - * @return bool|array False on failure; Array of URL components on success; - * See parse_url()'s return values. + * @param string $url The URL to parse. + * @param int $component The specific component to retrieve. Use one of the PHP + * predefined constants to specify which one. + * Defaults to -1 (= return all parts as an array). + * @see http://php.net/manual/en/function.parse-url.php + * @return mixed False on failure; Array of URL components on success; + * When a specific component has been requested: null if the component doesn't + * exist in the given URL; a sting or - in the case of PHP_URL_PORT - integer + * when it does; See parse_url()'s return values. */ -function wp_parse_url( $url ) { - $parts = @parse_url( $url ); - if ( ! $parts ) { - // < PHP 5.4.7 compat, trouble with relative paths including a scheme break in the path +function wp_parse_url( $url, $component = -1 ) { + $parts = @parse_url( $url, $component ); + + if ( version_compare( PHP_VERSION, '5.4.7', '>=' ) ) { + return $parts; + } + + if ( false === $parts ) { + // < PHP 5.4.7 compat, trouble with relative paths including a scheme break in the path. if ( '/' == $url[0] && false !== strpos( $url, '://' ) ) { - // Since we know it's a relative path, prefix with a scheme/host placeholder and try again - if ( ! $parts = @parse_url( 'placeholder://placeholder' . $url ) ) { + if ( in_array( $component, array( PHP_URL_SCHEME, PHP_URL_HOST ), true ) ) { + return null; + } + // Since we know it's a relative path, prefix with a scheme/host placeholder and try again. + if ( ! $parts = @parse_url( 'placeholder://placeholder' . $url, $component ) ) { return $parts; } - // Remove the placeholder values - unset( $parts['scheme'], $parts['host'] ); + // Remove the placeholder values. + if ( -1 === $component ) { + unset( $parts['scheme'], $parts['host'] ); + } } else { return $parts; } } - // < PHP 5.4.7 compat, doesn't detect schemeless URL's host field - if ( '//' == substr( $url, 0, 2 ) && ! isset( $parts['host'] ) ) { - $path_parts = explode( '/', substr( $parts['path'], 2 ), 2 ); - $parts['host'] = $path_parts[0]; - if ( isset( $path_parts[1] ) ) { - $parts['path'] = '/' . $path_parts[1]; - } else { - unset( $parts['path'] ); + // < PHP 5.4.7 compat, doesn't detect a schemeless URL's host field. + if ( '//' == substr( $url, 0, 2 ) ) { + if ( -1 === $component && ! isset( $parts['host'] ) ) { + $path_parts = explode( '/', substr( $parts['path'], 2 ), 2 ); + $parts['host'] = $path_parts[0]; + if ( isset( $path_parts[1] ) ) { + $parts['path'] = '/' . $path_parts[1]; + } else { + unset( $parts['path'] ); + } + } elseif ( PHP_URL_HOST === $component || PHP_URL_PATH === $component ) { + $all_parts = @parse_url( $url ); + if ( ! isset( $all_parts['host'] ) ) { + $path_parts = explode( '/', substr( $all_parts['path'], 2 ), 2 ); + if ( PHP_URL_PATH === $component ) { + if ( isset( $path_parts[1] ) ) { + $parts = '/' . $path_parts[1]; + } else { + $parts = null; + } + } elseif ( PHP_URL_HOST === $component ) { + $parts = $path_parts[0]; + } + } } } diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index ed0b9eeefb..607eb47d9f 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -6,6 +6,8 @@ */ class Tests_HTTP_HTTP extends WP_UnitTestCase { + const FULL_TEST_URL = 'http://username:password@host.name:9090/path?arg1=value1&arg2=value2#anchor'; + /** * @dataProvider make_absolute_url_testcases */ @@ -78,6 +80,16 @@ class Tests_HTTP_HTTP extends WP_UnitTestCase { function parse_url_testcases() { // 0: The URL, 1: The expected resulting structure return array( + array( self::FULL_TEST_URL, array( + 'scheme' => 'http', + 'host' => 'host.name', + 'port' => 9090, + 'user' => 'username', + 'pass' => 'password', + 'path' => '/path', + 'query' => 'arg1=value1&arg2=value2', + 'fragment' => 'anchor', + ) ), array( 'http://example.com/', array( 'scheme' => 'http', 'host' => 'example.com', 'path' => '/' ) ), // < PHP 5.4.7: Schemeless URL @@ -85,7 +97,7 @@ class Tests_HTTP_HTTP extends WP_UnitTestCase { array( '//example.com/', array( 'host' => 'example.com', 'path' => '/' ) ), array( 'http://example.com//path/', array( 'scheme' => 'http', 'host' => 'example.com', 'path' => '//path/' ) ), - // < PHP 5.4.7: Scheme seperator in the URL + // < PHP 5.4.7: Scheme separator in the URL. array( 'http://example.com/http://example.net/', array( 'scheme' => 'http', 'host' => 'example.com', 'path' => '/http://example.net/' ) ), array( '/path/http://example.net/', array( 'path' => '/path/http://example.net/' ) ), @@ -103,6 +115,69 @@ class Tests_HTTP_HTTP extends WP_UnitTestCase { */ } + /** + * @ticket 36356 + */ + function test_wp_parse_url_with_default_component() { + $actual = wp_parse_url( self::FULL_TEST_URL, -1 ); + $this->assertEquals( array( + 'scheme' => 'http', + 'host' => 'host.name', + 'port' => 9090, + 'user' => 'username', + 'pass' => 'password', + 'path' => '/path', + 'query' => 'arg1=value1&arg2=value2', + 'fragment' => 'anchor', + ), $actual ); + } + + /** + * @ticket 36356 + * + * @dataProvider parse_url_component_testcases + */ + function test_wp_parse_url_with_component( $url, $component, $expected ) { + $actual = wp_parse_url( $url, $component ); + $this->assertSame( $expected, $actual ); + } + + function parse_url_component_testcases() { + // 0: The URL, 1: The requested component, 2: The expected resulting structure. + return array( + array( self::FULL_TEST_URL, PHP_URL_SCHEME, 'http' ), + array( self::FULL_TEST_URL, PHP_URL_USER, 'username' ), + array( self::FULL_TEST_URL, PHP_URL_PASS, 'password' ), + array( self::FULL_TEST_URL, PHP_URL_HOST, 'host.name' ), + array( self::FULL_TEST_URL, PHP_URL_PORT, 9090 ), + array( self::FULL_TEST_URL, PHP_URL_PATH, '/path' ), + array( self::FULL_TEST_URL, PHP_URL_QUERY, 'arg1=value1&arg2=value2' ), + array( self::FULL_TEST_URL, PHP_URL_FRAGMENT, 'anchor' ), + + // < PHP 5.4.7: Schemeless URL. + array( '//example.com/path/', PHP_URL_HOST, 'example.com' ), + array( '//example.com/path/', PHP_URL_PATH, '/path/' ), + array( '//example.com/', PHP_URL_HOST, 'example.com' ), + array( '//example.com/', PHP_URL_PATH, '/' ), + array( 'http://example.com//path/', PHP_URL_HOST, 'example.com' ), + array( 'http://example.com//path/', PHP_URL_PATH, '//path/' ), + + // < PHP 5.4.7: Scheme separator in the URL. + array( 'http://example.com/http://example.net/', PHP_URL_HOST, 'example.com' ), + array( 'http://example.com/http://example.net/', PHP_URL_PATH, '/http://example.net/' ), + array( '/path/http://example.net/', PHP_URL_HOST, null ), + array( '/path/http://example.net/', PHP_URL_PATH, '/path/http://example.net/' ), + + // < PHP 5.4.7: IPv6 literals in schemeless URLs are handled incorrectly. + array( '//[::FFFF::127.0.0.1]/', PHP_URL_HOST, '[::FFFF::127.0.0.1]' ), + array( '//[::FFFF::127.0.0.1]/', PHP_URL_PATH, '/' ), + + // PHP's parse_url() calls this an invalid URL, we handle it as a path. + array( '/://example.com/', PHP_URL_PATH, '/://example.com/' ), + + ); + } + /** * @ticket 35426 */