Merge "Fix wrong @return type hints in Language::tsTo… methods"
[lhc/web/wiklou.git] / includes / utils / IP.php
index fd7030e..4a2205e 100644 (file)
@@ -73,7 +73,7 @@ class IP {
        /**
         * Determine if a string is as valid IP address or network (CIDR prefix).
         * SIIT IPv4-translated addresses are rejected.
-        * Note: canonicalize() tries to convert translated addresses to IPv4.
+        * @note canonicalize() tries to convert translated addresses to IPv4.
         *
         * @param string $ip Possible IP address
         * @return bool
@@ -84,7 +84,7 @@ class IP {
 
        /**
         * Given a string, determine if it as valid IP in IPv6 only.
-        * Note: Unlike isValid(), this looks for networks too.
+        * @note Unlike isValid(), this looks for networks too.
         *
         * @param string $ip Possible IP address
         * @return bool
@@ -95,7 +95,7 @@ class IP {
 
        /**
         * Given a string, determine if it as valid IP in IPv4 only.
-        * Note: Unlike isValid(), this looks for networks too.
+        * @note Unlike isValid(), this looks for networks too.
         *
         * @param string $ip Possible IP address
         * @return bool
@@ -107,7 +107,7 @@ class IP {
        /**
         * Validate an IP address. Ranges are NOT considered valid.
         * SIIT IPv4-translated addresses are rejected.
-        * Note: canonicalize() tries to convert translated addresses to IPv4.
+        * @note canonicalize() tries to convert translated addresses to IPv4.
         *
         * @param string $ip
         * @return bool True if it is valid
@@ -120,7 +120,7 @@ class IP {
        /**
         * Validate an IP Block (valid address WITH a valid prefix).
         * SIIT IPv4-translated addresses are rejected.
-        * Note: canonicalize() tries to convert translated addresses to IPv4.
+        * @note canonicalize() tries to convert translated addresses to IPv4.
         *
         * @param string $ipblock
         * @return bool True if it is valid
@@ -132,8 +132,9 @@ class IP {
 
        /**
         * Convert an IP into a verbose, uppercase, normalized form.
-        * IPv6 addresses in octet notation are expanded to 8 words.
-        * IPv4 addresses are just trimmed.
+        * Both IPv4 and IPv6 addresses are trimmed. Additionally,
+        * IPv6 addresses in octet notation are expanded to 8 words;
+        * IPv4 addresses have leading zeros, in each octet, removed.
         *
         * @param string $ip IP address in quad or octet form (CIDR or not).
         * @return string
@@ -143,8 +144,16 @@ class IP {
                if ( $ip === '' ) {
                        return null;
                }
-               if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) {
-                       return $ip; // nothing else to do for IPv4 addresses or invalid ones
+               /* If not an IP, just return trimmed value, since sanitizeIP() is called
+                * in a number of contexts where usernames are supplied as input.
+                */
+               if ( !self::isIPAddress( $ip ) ) {
+                       return $ip;
+               }
+               if ( self::isIPv4( $ip ) ) {
+                       // Remove leading 0's from octet representation of IPv4 address
+                       $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip );
+                       return $ip;
                }
                // Remove any whitespaces, convert to upper case
                $ip = strtoupper( $ip );
@@ -198,7 +207,7 @@ class IP {
                        if ( strpos( $ip, '/' ) !== false ) {
                                list( $ip, $cidr ) = explode( '/', $ip, 2 );
                        } else {
-                               list( $ip, $cidr ) = array( $ip, '' );
+                               list( $ip, $cidr ) = [ $ip, '' ];
                        }
                        // Get the largest slice of words with multiple zeros
                        $offset = 0;
@@ -248,9 +257,9 @@ class IP {
                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'] ) );
+                                       return [ $m[1], intval( $m['port'] ) ];
                                } else {
-                                       return array( $m[1], false );
+                                       return [ $m[1], false ];
                                }
                        } else {
                                // Square bracket found but no IPv6
@@ -261,7 +270,7 @@ class IP {
                if ( $numColons >= 2 ) {
                        // Is it a bare IPv6 address?
                        if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
-                               return array( $both, false );
+                               return [ $both, false ];
                        } else {
                                // Not valid IPv6, but too many colons for anything else
                                return false;
@@ -271,7 +280,7 @@ class IP {
                        // Host:port?
                        $bits = explode( ':', $both );
                        if ( preg_match( '/^\d+/', $bits[1] ) ) {
-                               return array( $bits[0], intval( $bits[1] ) );
+                               return [ $bits[0], intval( $bits[1] ) ];
                        } else {
                                // Not a valid port
                                return false;
@@ -279,7 +288,7 @@ class IP {
                }
 
                // Plain hostname
-               return array( $both, false );
+               return [ $both, false ];
        }
 
        /**
@@ -369,7 +378,7 @@ class IP {
        public static function isPublic( $ip ) {
                static $privateSet = null;
                if ( !$privateSet ) {
-                       $privateSet = new IPSet( array(
+                       $privateSet = new IPSet( [
                                '10.0.0.0/8', # RFC 1918 (private)
                                '172.16.0.0/12', # RFC 1918 (private)
                                '192.168.0.0/16', # RFC 1918 (private)
@@ -379,7 +388,7 @@ class IP {
                                '0:0:0:0:0:0:0:1', # loopback
                                '169.254.0.0/16', # link-local
                                'fe80::/10', # link-local
-                       ) );
+                       ] );
                }
                return !$privateSet->match( $ip );
        }
@@ -399,8 +408,9 @@ class IP {
                if ( self::isIPv6( $ip ) ) {
                        $n = 'v6-' . self::IPv6ToRawHex( $ip );
                } elseif ( self::isIPv4( $ip ) ) {
-                       // Bug 60035: an IP with leading 0's fails in ip2long sometimes (e.g. *.08)
-                       $ip = preg_replace( '/(?<=\.)0+(?=[1-9])/', '', $ip );
+                       // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
+                       // also double/triple 0 needs to be changed to just a single 0 for ip2long.
+                       $ip = self::sanitizeIP( $ip );
                        $n = ip2long( $ip );
                        if ( $n < 0 ) {
                                $n += pow( 2, 32 );
@@ -453,7 +463,7 @@ class IP {
                }
                $parts = explode( '/', $range, 2 );
                if ( count( $parts ) != 2 ) {
-                       return array( false, false );
+                       return [ false, false ];
                }
                list( $network, $bits ) = $parts;
                $network = ip2long( $network );
@@ -472,7 +482,7 @@ class IP {
                        $bits = false;
                }
 
-               return array( $network, $bits );
+               return [ $network, $bits ];
        }
 
        /**
@@ -523,9 +533,9 @@ class IP {
                        $start = $end = self::toHex( $range );
                }
                if ( $start === false || $end === false ) {
-                       return array( false, false );
+                       return [ false, false ];
                } else {
-                       return array( $start, $end );
+                       return [ $start, $end ];
                }
        }
 
@@ -541,7 +551,7 @@ class IP {
                # Explode into <expanded IP,range>
                $parts = explode( '/', IP::sanitizeIP( $range ), 2 );
                if ( count( $parts ) != 2 ) {
-                       return array( false, false );
+                       return [ false, false ];
                }
                list( $network, $bits ) = $parts;
                $network = self::IPv6ToRawHex( $network );
@@ -562,7 +572,7 @@ class IP {
                        $bits = false;
                }
 
-               return array( $network, (int)$bits );
+               return [ $network, (int)$bits ];
        }
 
        /**
@@ -611,9 +621,9 @@ class IP {
                        $start = $end = self::toHex( $range );
                }
                if ( $start === false || $end === false ) {
-                       return array( false, false );
+                       return [ false, false ];
                } else {
-                       return array( $start, $end );
+                       return [ $start, $end ];
                }
        }
 
@@ -623,6 +633,9 @@ class IP {
         * @param string $addr The address to check against the given range.
         * @param string $range The range to check the given address against.
         * @return bool Whether or not the given address is in the given range.
+        *
+        * @note This can return unexpected results for invalid arguments!
+        *       Make sure you pass a valid IP address and IP range.
         */
        public static function isInRange( $addr, $range ) {
                $hexIP = self::toHex( $addr );
@@ -676,7 +689,7 @@ class IP {
                        }
                }
                // IPv6 loopback address
-               $m = array();
+               $m = [];
                if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
                        return '127.0.0.1';
                }
@@ -721,7 +734,7 @@ class IP {
         */
        public static function isTrustedProxy( $ip ) {
                $trusted = self::isConfiguredProxy( $ip );
-               Hooks::run( 'IsTrustedProxy', array( &$ip, &$trusted ) );
+               Hooks::run( 'IsTrustedProxy', [ &$ip, &$trusted ] );
                return $trusted;
        }
 
@@ -756,4 +769,23 @@ class IP {
        public static function clearCaches() {
                self::$proxyIpSet = null;
        }
+
+       /**
+        * Returns the subnet of a given IP
+        *
+        * @param string $ip
+        * @return string|false
+        */
+       public static function getSubnet( $ip ) {
+               $matches = [];
+               $subnet = false;
+               if ( IP::isIPv6( $ip ) ) {
+                       $parts = IP::parseRange( "$ip/64" );
+                       $subnet = $parts[0];
+               } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
+                       // IPv4
+                       $subnet = $matches[1];
+               }
+               return $subnet;
+       }
 }