+ return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock )
+ || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) );
+ }
+
+ /**
+ * Convert an IP into a nice standard form.
+ * IPv6 addresses in octet notation are expanded to 8 words.
+ * IPv4 addresses are just trimmed.
+ *
+ * @param $ip String: IP address in quad or octet form (CIDR or not).
+ * @return String
+ */
+ public static function sanitizeIP( $ip ) {
+ $ip = trim( $ip );
+ if ( $ip === '' ) {
+ return null;
+ }
+ if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) {
+ return $ip; // nothing else to do for IPv4 addresses or invalid ones
+ }
+ // Remove any whitespaces, convert to upper case
+ $ip = strtoupper( $ip );
+ // Expand zero abbreviations
+ $abbrevPos = strpos( $ip, '::' );
+ if ( $abbrevPos !== false ) {
+ // We know this is valid IPv6. Find the last index of the
+ // address before any CIDR number (e.g. "a:b:c::/24").
+ $CIDRStart = strpos( $ip, "/" );
+ $addressEnd = ( $CIDRStart !== false )
+ ? $CIDRStart - 1
+ : strlen( $ip ) - 1;
+ // If the '::' is at the beginning...
+ if ( $abbrevPos == 0 ) {
+ $repeat = '0:';
+ $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::'
+ $pad = 9; // 7+2 (due to '::')
+ // If the '::' is at the end...
+ } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) {
+ $repeat = ':0';
+ $extra = '';
+ $pad = 9; // 7+2 (due to '::')
+ // If the '::' is in the middle...
+ } else {
+ $repeat = ':0';
+ $extra = ':';
+ $pad = 8; // 6+2 (due to '::')
+ }
+ $ip = str_replace( '::',
+ str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra,
+ $ip
+ );
+ }
+ // Remove leading zereos from each bloc as needed
+ $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
+ return $ip;
+ }
+
+ /**
+ * Given a host/port string, like one might find in the host part of a URL
+ * per RFC 2732, split the hostname part and the port part and return an
+ * array with an element for each. If there is no port part, the array will
+ * have false in place of the port. If the string was invalid in some way,
+ * false is returned.
+ *
+ * This was easy with IPv4 and was generally done in an ad-hoc way, but
+ * with IPv6 it's somewhat more complicated due to the need to parse the
+ * square brackets and colons.
+ *
+ * A bare IPv6 address is accepted despite the lack of square brackets.
+ *
+ * @param $both string The string with the host and port
+ * @return array
+ */
+ public static function splitHostAndPort( $both ) {
+ if ( substr( $both, 0, 1 ) === '[' ) {
+ if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) {
+ if ( isset( $m['port'] ) ) {
+ return array( $m[1], intval( $m['port'] ) );
+ } else {
+ return array( $m[1], false );
+ }
+ } else {
+ // Square bracket found but no IPv6
+ return false;
+ }
+ }
+ $numColons = substr_count( $both, ':' );
+ if ( $numColons >= 2 ) {
+ // Is it a bare IPv6 address?
+ if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
+ return array( $both, false );
+ } else {
+ // Not valid IPv6, but too many colons for anything else
+ return false;
+ }
+ }
+ if ( $numColons >= 1 ) {
+ // Host:port?
+ $bits = explode( ':', $both );
+ if ( preg_match( '/^\d+/', $bits[1] ) ) {
+ return array( $bits[0], intval( $bits[1] ) );
+ } else {
+ // Not a valid port
+ return false;
+ }
+ }
+ // Plain hostname
+ return array( $both, false );
+ }
+
+ /**
+ * Given a host name and a port, combine them into host/port string like
+ * you might find in a URL. If the host contains a colon, wrap it in square
+ * brackets like in RFC 2732. If the port matches the default port, omit
+ * the port specification
+ *
+ * @param $host string
+ * @param $port int
+ * @param $defaultPort bool|int
+ * @return string
+ */
+ public static function combineHostAndPort( $host, $port, $defaultPort = false ) {
+ if ( strpos( $host, ':' ) !== false ) {
+ $host = "[$host]";
+ }
+ if ( $defaultPort !== false && $port == $defaultPort ) {
+ return $host;
+ } else {
+ return "$host:$port";
+ }
+ }
+
+ /**
+ * Given an unsigned integer, returns an IPv6 address in octet notation
+ *
+ * @param $ip_int String: IP address.
+ * @return String
+ */
+ public static function toOctet( $ip_int ) {
+ return self::hexToOctet( wfBaseConvert( $ip_int, 10, 16, 32, false ) );
+ }
+
+ /**
+ * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
+ *
+ * @param $hex String: number, with "v6-" prefix if it is IPv6
+ * @return String: quad-dotted (IPv4) or octet notation (IPv6)
+ */
+ public static function formatHex( $hex ) {
+ if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6
+ return self::hexToOctet( substr( $hex, 3 ) );
+ } else { // IPv4
+ return self::hexToQuad( $hex );
+ }
+ }
+
+ /**
+ * Converts a hexadecimal number to an IPv6 address in octet notation
+ *
+ * @param $ip_hex String: pure hex (no v6- prefix)
+ * @return String (of format a:b:c:d:e:f:g:h)
+ */
+ public static function hexToOctet( $ip_hex ) {
+ // Pad hex to 32 chars (128 bits)
+ $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT );
+ // Separate into 8 words
+ $ip_oct = substr( $ip_hex, 0, 4 );
+ for ( $n = 1; $n < 8; $n++ ) {
+ $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 );
+ }
+ // NO leading zeroes
+ $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct );
+ return $ip_oct;
+ }
+
+ /**
+ * Converts a hexadecimal number to an IPv4 address in quad-dotted notation
+ *
+ * @param $ip_hex String: pure hex
+ * @return String (of format a.b.c.d)
+ */
+ public static function hexToQuad( $ip_hex ) {
+ // Pad hex to 8 chars (32 bits)
+ $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT );
+ // Separate into four quads
+ $s = '';
+ for ( $i = 0; $i < 4; $i++ ) {
+ if ( $s !== '' ) {
+ $s .= '.';
+ }
+ $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 );
+ }
+ return $s;