define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)');
define( 'RE_IPV6_ADD',
- '(?:' . // starts with "::" (includes the address "::")
- '::|:(?::' . RE_IPV6_WORD . '){1,7}' .
- '|' . // ends with "::" (not including the address "::")
+ '(?:' . // starts with "::" (including "::")
+ ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
+ '|' . // ends with "::" (except "::")
RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
- '|' . // has no "::"
- RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
- '|' . // contains one "::" in the middle (awkward regex for PCRE 4.0+ compatibility)
- RE_IPV6_WORD . '(?::(?P<abn>(?!(?P=abn)):(?P<iabn>))?' . RE_IPV6_WORD . '){1,6}(?P=iabn)' .
+ '|' . // contains one "::" in the middle, ending in "::WORD"
+ RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,5}' . '::' . RE_IPV6_WORD .
+ '|' . // contains one "::" in the middle, not ending in "::WORD" (regex for PCRE 4.0+)
+ RE_IPV6_WORD . '(?::(?P<abn>:(?P<iabn>))?' . RE_IPV6_WORD . '(?!:(?P=abn))){1,5}' .
+ ':' . RE_IPV6_WORD . '(?P=iabn)' .
// NOTE: (?!(?P=abn)) fails iff "::" used twice; (?P=iabn) passes iff a "::" was found.
-
- // Better regexp (PCRE 7.2+ only), allows intuitive regex concatenation
- #RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
+ '|' . // contains no "::"
+ RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
')'
+ // NOTE: With PCRE 7.2+, we can combine the two '"::" in the middle' cases into:
+ // RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)'
+ // This also improves regex concatenation by using relative references.
);
// An IPv6 block is an IP address and a prefix (d1 to d128)
define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
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 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
*
return $n;
}
+ /**
+ * @param $ip
+ * @return String
+ */
private static function toUnsigned6( $ip ) {
return wfBaseConvert( self::IPv6ToRawHex( $ip ), 16, 10 );
}
* Convert a network specification in IPv6 CIDR notation to an
* integer network and a number of bits
*
+ * @param $range
+ *
* @return array(string, int)
*/
private static function parseCIDR6( $range ) {
* 2001:0db8:85a3::7344/96 CIDR
* 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
* 2001:0db8:85a3::7344/96 Single IP
+ *
+ * @param $range
+ *
* @return array(string, string)
*/
private static function parseRange6( $range ) {
// IPv6 loopback address
$m = array();
if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
- return '127.0.0.1';
+ return '127.0.0.1';
}
// IPv4-mapped and IPv4-compatible IPv6 addresses
if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) {
return null; // give up
}
+
+ /**
+ * Gets rid of uneeded numbers in quad-dotted/octet IP strings
+ * For example, 127.111.113.151/24 -> 127.111.113.0/24
+ * @param $range String: IP address to normalize
+ * @return string
+ */
+ public static function sanitizeRange( $range ){
+ list( /*...*/, $bits ) = self::parseCIDR( $range );
+ list( $start, /*...*/ ) = self::parseRange( $range );
+ $start = self::formatHex( $start );
+ return "$start/$bits";
+ }
}