diff --git a/src/wp-includes/wp-db.php b/src/wp-includes/wp-db.php index c601bcaba1..e96714fa23 100644 --- a/src/wp-includes/wp-db.php +++ b/src/wp-includes/wp-db.php @@ -1460,24 +1460,23 @@ class wpdb { if ( $this->use_mysqli ) { $this->dbh = mysqli_init(); - // mysqli_real_connect doesn't support the host param including a port or socket - // like mysql_connect does. This duplicates how mysql_connect detects a port and/or socket file. - $port = null; - $socket = null; - $host = $this->dbhost; - $port_or_socket = strstr( $host, ':' ); - if ( ! empty( $port_or_socket ) ) { - $host = substr( $host, 0, strpos( $host, ':' ) ); - $port_or_socket = substr( $port_or_socket, 1 ); - if ( 0 !== strpos( $port_or_socket, '/' ) ) { - $port = intval( $port_or_socket ); - $maybe_socket = strstr( $port_or_socket, ':' ); - if ( ! empty( $maybe_socket ) ) { - $socket = substr( $maybe_socket, 1 ); - } - } else { - $socket = $port_or_socket; - } + $host = $this->dbhost; + $port = null; + $socket = null; + $is_ipv6 = false; + + if ( $host_data = $this->parse_db_host( $this->dbhost ) ) { + list( $host, $port, $socket, $is_ipv6 ) = $host_data; + } + + /* + * If using the `mysqlnd` library, the IPv6 address needs to be + * enclosed in square brackets, whereas it doesn't while using the + * `libmysqlclient` library. + * @see https://bugs.php.net/bug.php?id=67563 + */ + if ( $is_ipv6 && extension_loaded( 'mysqlnd' ) ) { + $host = "[$host]"; } if ( WP_DEBUG ) { @@ -1489,7 +1488,8 @@ class wpdb { if ( $this->dbh->connect_errno ) { $this->dbh = null; - /* It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if: + /* + * It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if: * - We haven't previously connected, and * - WP_USE_EXT_MYSQL isn't set to false, and * - ext/mysql is loaded. @@ -1569,6 +1569,52 @@ class wpdb { return false; } + /** + * Parse the DB_HOST setting to interpret it for mysqli_real_connect. + * + * mysqli_real_connect doesn't support the host param including a port or + * socket like mysql_connect does. This duplicates how mysql_connect detects + * a port and/or socket file. + * + * @since 4.9.0 + * + * @param string $host The DB_HOST setting to parse. + * @return array|bool Array containing the host, the port, the socket and whether + * it is an IPv6 address, in that order. If $host couldn't be parsed, + * returns false. + */ + public function parse_db_host( $host ) { + $port = null; + $socket = null; + $is_ipv6 = false; + + // We need to check for an IPv6 address first. + // An IPv6 address will always contain at least two colons. + if ( substr_count( $host, ':' ) > 1 ) { + $pattern = '#^(?:\[)?(?[0-9a-fA-F:]+)(?:\]:(?[\d]+))?(?:/(?.+))?#'; + $is_ipv6 = true; + } else { + // We seem to be dealing with an IPv4 address. + $pattern = '#^(?[^:/]*)(?::(?[\d]+))?(?::(?.+))?#'; + } + + $matches = array(); + $result = preg_match( $pattern, $host, $matches ); + + if ( 1 !== $result ) { + // Couldn't parse the address, bail. + return false; + } + + foreach ( array( 'host', 'port', 'socket' ) as $component ) { + if ( array_key_exists( $component, $matches ) ) { + $$component = $matches[$component]; + } + } + + return array( $host, $port, $socket, $is_ipv6 ); + } + /** * Checks that the connection to the database is still up. If not, try to reconnect. * diff --git a/tests/phpunit/tests/db.php b/tests/phpunit/tests/db.php index a2626a8236..01dd3688d3 100644 --- a/tests/phpunit/tests/db.php +++ b/tests/phpunit/tests/db.php @@ -1126,4 +1126,166 @@ class Tests_DB extends WP_UnitTestCase { $sql = $wpdb->prepare( '%d %1$d %%% %', 1 ); $this->assertEquals( '1 %1$d %% %', $sql ); } + + /** + * @dataProvider parse_db_host_data_provider + * @ticket 41722 + */ + public function test_parse_db_host( $host_string, $expect_bail, $host, $port, $socket, $is_ipv6 ) { + global $wpdb; + $data = $wpdb->parse_db_host( $host_string ); + if ( $expect_bail ) { + $this->assertFalse( $data ); + } else { + $this->assertInternalType( 'array', $data ); + + list( $parsed_host, $parsed_port, $parsed_socket, $parsed_is_ipv6 ) = $data; + + $this->assertEquals( $host, $parsed_host ); + $this->assertEquals( $port, $parsed_port ); + $this->assertEquals( $socket, $parsed_socket ); + $this->assertEquals( $is_ipv6, $parsed_is_ipv6 ); + } + } + + public function parse_db_host_data_provider() { + return array( + array( + '', // DB_HOST + false, // Expect parse_db_host to bail for this hostname + null, // Parsed host + null, // Parsed port + null, // Parsed socket + false, // is_ipv6 + ), + array( + ':3306', + false, + null, + '3306', + null, + false, + ), + array( + ':/tmp/mysql.sock', + false, + null, + null, + '/tmp/mysql.sock', + false, + ), + array( + '127.0.0.1', + false, + '127.0.0.1', + null, + null, + false, + ), + array( + '127.0.0.1:3306', + false, + '127.0.0.1', + '3306', + null, + false, + ), + array( + 'example.com', + false, + 'example.com', + null, + null, + false, + ), + array( + 'example.com:3306', + false, + 'example.com', + '3306', + null, + false, + ), + array( + 'localhost', + false, + 'localhost', + null, + null, + false, + ), + array( + 'localhost:/tmp/mysql.sock', + false, + 'localhost', + null, + '/tmp/mysql.sock', + false, + ), + array( + '0000:0000:0000:0000:0000:0000:0000:0001', + false, + '0000:0000:0000:0000:0000:0000:0000:0001', + null, + null, + true, + ), + array( + '::1', + false, + '::1', + null, + null, + true, + ), + array( + '[::1]', + false, + '::1', + null, + null, + true, + ), + array( + '[::1]:3306', + false, + '::1', + '3306', + null, + true, + ), + array( + '2001:0db8:0000:0000:0000:ff00:0042:8329', + false, + '2001:0db8:0000:0000:0000:ff00:0042:8329', + null, + null, + true, + ), + array( + '2001:db8:0:0:0:ff00:42:8329', + false, + '2001:db8:0:0:0:ff00:42:8329', + null, + null, + true, + ), + array( + '2001:db8::ff00:42:8329', + false, + '2001:db8::ff00:42:8329', + null, + null, + true, + ), + array( + '?::', + true, + null, + null, + null, + false, + ), + ); + } }