<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[41629] trunk: Database: Add support for connecting to IPv6 hosts</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/41629">41629</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/41629","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>pento</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-09-28 05:36:34 +0000 (Thu, 28 Sep 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Database: Add support for connecting to IPv6 hosts

IPv4 addresses are scarce, overworked, and underpaid. They're ready to retire, but we just won't let them go. If you care about their wellbeing, switch to IPv6 today.

Props schlessera, birgire.
Fixes <a href="https://core.trac.wordpress.org/ticket/41722">#41722</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeswpdbphp">trunk/src/wp-includes/wp-db.php</a></li>
<li><a href="#trunktestsphpunittestsdbphp">trunk/tests/phpunit/tests/db.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeswpdbphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/wp-db.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/wp-db.php   2017-09-28 04:31:05 UTC (rev 41628)
+++ trunk/src/wp-includes/wp-db.php     2017-09-28 05:36:34 UTC (rev 41629)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1460,26 +1460,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $this->use_mysqli ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->dbh = mysqli_init();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // 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;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $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;
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        /*
+                        * 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]";
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( WP_DEBUG ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
</span><span class="cx" style="display: block; padding: 0 10px">                        } else {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1489,7 +1488,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $this->dbh->connect_errno ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->dbh = null;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /* It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if:
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         /*
+                                * It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if:
</ins><span class="cx" style="display: block; padding: 0 10px">                                  *  - We haven't previously connected, and
</span><span class="cx" style="display: block; padding: 0 10px">                                 *  - WP_USE_EXT_MYSQL isn't set to false, and
</span><span class="cx" style="display: block; padding: 0 10px">                                 *  - ext/mysql is loaded.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1570,6 +1570,52 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * 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 = '#^(?:\[)?(?<host>[0-9a-fA-F:]+)(?:\]:(?<port>[\d]+))?(?:/(?<socket>.+))?#';
+                       $is_ipv6 = true;
+               } else {
+                       // We seem to be dealing with an IPv4 address.
+                       $pattern = '#^(?<host>[^:/]*)(?::(?<port>[\d]+))?(?::(?<socket>.+))?#';
+               }
+
+               $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 );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Checks that the connection to the database is still up. If not, try to reconnect.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * If this function is unable to reconnect, it will forcibly die, or if after the
</span></span></pre></div>
<a id="trunktestsphpunittestsdbphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/db.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/db.php  2017-09-28 04:31:05 UTC (rev 41628)
+++ trunk/tests/phpunit/tests/db.php    2017-09-28 05:36:34 UTC (rev 41629)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1126,4 +1126,166 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $sql = $wpdb->prepare( '%d %1$d %%% %', 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( '1 %1$d %% %', $sql );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @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,
+                       ),
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>