| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Contains a simple class providing some useful internet protocol-related functions.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package   core
 | 
        
           |  |  | 21 |  * @copyright 2016 Jake Dallimore
 | 
        
           |  |  | 22 |  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 23 |  * @author    Jake Dallimore <jrhdallimore@gmail.com>
 | 
        
           |  |  | 24 |  */
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 | namespace core;
 | 
        
           |  |  | 27 |   | 
        
           |  |  | 28 | defined('MOODLE_INTERNAL') || exit();
 | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 | /**
 | 
        
           |  |  | 31 |  * Static helper class providing some useful internet-protocol-related functions.
 | 
        
           |  |  | 32 |  *
 | 
        
           |  |  | 33 |  * @package   core
 | 
        
           |  |  | 34 |  * @copyright 2016 Jake Dallimore
 | 
        
           |  |  | 35 |  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 36 |  * @author    Jake Dallimore <jrhdallimore@gmail.com>
 | 
        
           |  |  | 37 |  */
 | 
        
           |  |  | 38 | final class ip_utils {
 | 
        
           |  |  | 39 |     /**
 | 
        
           |  |  | 40 |      * Syntax checking for domain names, including fully qualified domain names.
 | 
        
           |  |  | 41 |      *
 | 
        
           |  |  | 42 |      * This function does not verify the existence of the domain name. It only verifies syntactic correctness.
 | 
        
           |  |  | 43 |      * This is based on RFC1034/1035 and does not provide support for validation of internationalised domain names (IDNs).
 | 
        
           |  |  | 44 |      * All IDNs must be prior-converted to their ascii-compatible encoding before being passed to this function.
 | 
        
           |  |  | 45 |      *
 | 
        
           |  |  | 46 |      * @param string $domainname the input string to check.
 | 
        
           |  |  | 47 |      * @return bool true if the string has valid syntax, false otherwise.
 | 
        
           |  |  | 48 |      */
 | 
        
           |  |  | 49 |     public static function is_domain_name($domainname) {
 | 
        
           |  |  | 50 |         if (!is_string($domainname)) {
 | 
        
           |  |  | 51 |             return false;
 | 
        
           |  |  | 52 |         }
 | 
        
           |  |  | 53 |         // Usually the trailing dot (null label) is omitted, but is valid if supplied. We'll just remove it and validate as normal.
 | 
        
           |  |  | 54 |         $domainname = rtrim($domainname, '.');
 | 
        
           |  |  | 55 |   | 
        
           |  |  | 56 |         // The entire name cannot exceed 253 ascii characters (255 octets, less the leading label-length byte and null label byte).
 | 
        
           |  |  | 57 |         if (strlen($domainname) > 253) {
 | 
        
           |  |  | 58 |             return false;
 | 
        
           |  |  | 59 |         }
 | 
        
           |  |  | 60 |         // Tertiary domain labels can have 63 octets max, and must not have begin or end with a hyphen.
 | 
        
           |  |  | 61 |         // The TLD label cannot begin with a number, but otherwise, is only loosely restricted here (TLD list is not checked).
 | 
        
           |  |  | 62 |         $domaintertiary = '([a-zA-Z0-9](([a-zA-Z0-9-]{0,61})[a-zA-Z0-9])?\.)*';
 | 
        
           |  |  | 63 |         $domaintoplevel = '([a-zA-Z](([a-zA-Z0-9-]*)[a-zA-Z0-9])?)';
 | 
        
           |  |  | 64 |         $address = '(' . $domaintertiary .  $domaintoplevel . ')';
 | 
        
           |  |  | 65 |         $regexp = '#^' . $address . '$#i'; // Case insensitive matching.
 | 
        
           |  |  | 66 |         return preg_match($regexp, $domainname, $match) == true; // False for error, 0 for no match - we treat the same.
 | 
        
           |  |  | 67 |     }
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |     /**
 | 
        
           |  |  | 70 |      * Checks whether the input string is a valid wildcard domain matching pattern.
 | 
        
           |  |  | 71 |      *
 | 
        
           |  |  | 72 |      * A domain matching pattern is essentially a domain name with a single, leading wildcard (*) label, and at least one other
 | 
        
           |  |  | 73 |      * label. The wildcard label is considered to match at least one label at or above (to the left of) its position in the string,
 | 
        
           |  |  | 74 |      * but will not match the trailing domain (everything to its right).
 | 
        
           |  |  | 75 |      *
 | 
        
           |  |  | 76 |      * The string must be dot-separated, and the whole pattern must follow the domain name syntax rules defined in RFC1034/1035.
 | 
        
           |  |  | 77 |      * Namely, the character type (ascii), total-length (253) and label-length (63) restrictions. This function only confirms
 | 
        
           |  |  | 78 |      * syntactic correctness. It does not check for the existence of the domain/subdomains.
 | 
        
           |  |  | 79 |      *
 | 
        
           |  |  | 80 |      * For example, the string '*.example.com' is a pattern deemed to match any direct subdomain of
 | 
        
           |  |  | 81 |      * example.com (such as test.example.com), any higher level subdomains (e.g. another.test.example.com) but will not match
 | 
        
           |  |  | 82 |      * the 'example.com' domain itself.
 | 
        
           |  |  | 83 |      *
 | 
        
           |  |  | 84 |      * @param string $pattern the string to check.
 | 
        
           |  |  | 85 |      * @return bool true if the input string is a valid domain wildcard matching pattern, false otherwise.
 | 
        
           |  |  | 86 |      */
 | 
        
           |  |  | 87 |     public static function is_domain_matching_pattern($pattern) {
 | 
        
           |  |  | 88 |         if (!is_string($pattern)) {
 | 
        
           |  |  | 89 |             return false;
 | 
        
           |  |  | 90 |         }
 | 
        
           |  |  | 91 |         // Usually the trailing dot (null label) is omitted, but is valid if supplied. We'll just remove it and validate as normal.
 | 
        
           |  |  | 92 |         $pattern = rtrim($pattern, '.');
 | 
        
           |  |  | 93 |   | 
        
           |  |  | 94 |         // The entire name cannot exceed 253 ascii characters (255 octets, less the leading label-length byte and null label byte).
 | 
        
           |  |  | 95 |         if (strlen($pattern) > 253) {
 | 
        
           |  |  | 96 |             return false;
 | 
        
           |  |  | 97 |         }
 | 
        
           |  |  | 98 |         // A valid pattern must left-positioned wildcard symbol (*).
 | 
        
           |  |  | 99 |         // Tertiary domain labels can have 63 octets max, and must not have begin or end with a hyphen.
 | 
        
           |  |  | 100 |         // The TLD label cannot begin with a number, but otherwise, is only loosely restricted here (TLD list is not checked).
 | 
        
           |  |  | 101 |         $wildcard = '((\*)\.){1}';
 | 
        
           |  |  | 102 |         $domaintertiary = '([a-zA-Z0-9](([a-zA-Z0-9-]{0,61})[a-zA-Z0-9])?\.)*';
 | 
        
           |  |  | 103 |         $domaintoplevel = '([a-zA-Z](([a-zA-Z0-9-]*)[a-zA-Z0-9])?)';
 | 
        
           |  |  | 104 |         $address = '(' . $wildcard . $domaintertiary .  $domaintoplevel . ')';
 | 
        
           |  |  | 105 |         $regexp = '#^' . $address . '$#i'; // Case insensitive matching.
 | 
        
           |  |  | 106 |         return preg_match($regexp, $pattern, $match) == true; // False for error, 0 for no match - we treat the same.
 | 
        
           |  |  | 107 |     }
 | 
        
           |  |  | 108 |   | 
        
           |  |  | 109 |     /**
 | 
        
           |  |  | 110 |      * Syntax validation for IP addresses, supporting both IPv4 and Ipv6 formats.
 | 
        
           |  |  | 111 |      *
 | 
        
           |  |  | 112 |      * @param string $address the address to check.
 | 
        
           |  |  | 113 |      * @return bool true if the address is a valid IPv4 of IPv6 address, false otherwise.
 | 
        
           |  |  | 114 |      */
 | 
        
           |  |  | 115 |     public static function is_ip_address($address) {
 | 
        
           |  |  | 116 |         return filter_var($address, FILTER_VALIDATE_IP) !== false;
 | 
        
           |  |  | 117 |     }
 | 
        
           |  |  | 118 |   | 
        
           |  |  | 119 |     /**
 | 
        
           |  |  | 120 |      * Syntax validation for IPv4 addresses.
 | 
        
           |  |  | 121 |      *
 | 
        
           |  |  | 122 |      * @param string $address the address to check.
 | 
        
           |  |  | 123 |      * @return bool true if the address is a valid IPv4 address, false otherwise.
 | 
        
           |  |  | 124 |      */
 | 
        
           |  |  | 125 |     public static function is_ipv4_address($address) {
 | 
        
           |  |  | 126 |         return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
 | 
        
           |  |  | 127 |     }
 | 
        
           |  |  | 128 |   | 
        
           |  |  | 129 |     /**
 | 
        
           |  |  | 130 |      * Syntax checking for IPv4 address ranges.
 | 
        
           |  |  | 131 |      * Supports CIDR notation and last-group ranges.
 | 
        
           |  |  | 132 |      * Eg. 127.0.0.0/24 or 127.0.0.80-255
 | 
        
           |  |  | 133 |      *
 | 
        
           |  |  | 134 |      * @param string $addressrange the address range to check.
 | 
        
           |  |  | 135 |      * @return bool true if the string is a valid range representation, false otherwise.
 | 
        
           |  |  | 136 |      */
 | 
        
           |  |  | 137 |     public static function is_ipv4_range($addressrange) {
 | 
        
           |  |  | 138 |         // Check CIDR notation.
 | 
        
           |  |  | 139 |         if (preg_match('#^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$#', $addressrange, $match)) {
 | 
        
           |  |  | 140 |             $address = "{$match[1]}.{$match[2]}.{$match[3]}.{$match[4]}";
 | 
        
           |  |  | 141 |             return self::is_ipv4_address($address) && $match[5] <= 32;
 | 
        
           |  |  | 142 |         }
 | 
        
           |  |  | 143 |         // Check last-group notation.
 | 
        
           |  |  | 144 |         if (preg_match('#^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})-(\d{1,3})$#', $addressrange, $match)) {
 | 
        
           |  |  | 145 |             $address = "{$match[1]}.{$match[2]}.{$match[3]}.{$match[4]}";
 | 
        
           |  |  | 146 |             return self::is_ipv4_address($address) && $match[5] <= 255 && $match[5] >= $match[4];
 | 
        
           |  |  | 147 |         }
 | 
        
           |  |  | 148 |         return false;
 | 
        
           |  |  | 149 |     }
 | 
        
           |  |  | 150 |   | 
        
           |  |  | 151 |     /**
 | 
        
           |  |  | 152 |      * Syntax validation for IPv6 addresses.
 | 
        
           |  |  | 153 |      * This function does not check whether the address is assigned, only its syntactical correctness.
 | 
        
           |  |  | 154 |      *
 | 
        
           |  |  | 155 |      * @param string $address the address to check.
 | 
        
           |  |  | 156 |      * @return bool true if the address is a valid IPv6 address, false otherwise.
 | 
        
           |  |  | 157 |      */
 | 
        
           |  |  | 158 |     public static function is_ipv6_address($address) {
 | 
        
           |  |  | 159 |         return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
 | 
        
           |  |  | 160 |     }
 | 
        
           |  |  | 161 |   | 
        
           |  |  | 162 |     /**
 | 
        
           |  |  | 163 |      * Syntax validation for IPv6 address ranges.
 | 
        
           |  |  | 164 |      * Supports CIDR notation and last-group ranges.
 | 
        
           |  |  | 165 |      * Eg. fe80::d98c/64 or fe80::d98c-ffee
 | 
        
           |  |  | 166 |      *
 | 
        
           |  |  | 167 |      * @param string $addressrange the IPv6 address range to check.
 | 
        
           |  |  | 168 |      * @return bool true if the string is a valid range representation, false otherwise.
 | 
        
           |  |  | 169 |      */
 | 
        
           |  |  | 170 |     public static function is_ipv6_range($addressrange) {
 | 
        
           |  |  | 171 |         // Check CIDR notation.
 | 
        
           |  |  | 172 |         $ipv6parts = explode('/', $addressrange);
 | 
        
           |  |  | 173 |         if (count($ipv6parts) == 2) {
 | 
        
           |  |  | 174 |             $range = (int)$ipv6parts[1];
 | 
        
           |  |  | 175 |             return self::is_ipv6_address($ipv6parts[0]) && (string)$range === $ipv6parts[1] && $range >= 0 && $range <= 128;
 | 
        
           |  |  | 176 |         }
 | 
        
           |  |  | 177 |         // Check last-group notation.
 | 
        
           |  |  | 178 |         $ipv6parts = explode('-', $addressrange);
 | 
        
           |  |  | 179 |         if (count($ipv6parts) == 2) {
 | 
        
           |  |  | 180 |             $addressparts = explode(':', $ipv6parts[0]);
 | 
        
           |  |  | 181 |             $rangestart = $addressparts[count($addressparts) - 1];
 | 
        
           |  |  | 182 |             $rangeend = $ipv6parts[1];
 | 
        
           |  |  | 183 |             return self::is_ipv6_address($ipv6parts[0]) && ctype_xdigit($rangestart) && ctype_xdigit($rangeend)
 | 
        
           |  |  | 184 |             && strlen($rangeend) <= 4 && strlen($rangestart) <= 4 && hexdec($rangeend) >= hexdec($rangestart);
 | 
        
           |  |  | 185 |         }
 | 
        
           |  |  | 186 |         return false;
 | 
        
           |  |  | 187 |     }
 | 
        
           |  |  | 188 |   | 
        
           |  |  | 189 |     /**
 | 
        
           |  |  | 190 |      * Checks the domain name against a list of allowed domains. The list of allowed domains may use wildcards
 | 
        
           |  |  | 191 |      * that match {@see is_domain_matching_pattern()}. Domains are compared in a case-insensitive manner
 | 
        
           |  |  | 192 |      *
 | 
        
           |  |  | 193 |      * @param  string $domain Domain address
 | 
        
           |  |  | 194 |      * @param  array $alloweddomains An array of allowed domains.
 | 
        
           |  |  | 195 |      * @return boolean True if the domain matches one of the entries in the allowed domains list.
 | 
        
           |  |  | 196 |      */
 | 
        
           |  |  | 197 |     public static function is_domain_in_allowed_list($domain, $alloweddomains) {
 | 
        
           |  |  | 198 |   | 
        
           |  |  | 199 |         if (!self::is_domain_name($domain)) {
 | 
        
           |  |  | 200 |             return false;
 | 
        
           |  |  | 201 |         }
 | 
        
           |  |  | 202 |   | 
        
           |  |  | 203 |         foreach ($alloweddomains as $alloweddomain) {
 | 
        
           |  |  | 204 |             if (strpos($alloweddomain, '*') !== false) {
 | 
        
           |  |  | 205 |                 if (!self::is_domain_matching_pattern($alloweddomain)) {
 | 
        
           |  |  | 206 |                     continue;
 | 
        
           |  |  | 207 |                 }
 | 
        
           |  |  | 208 |                 // Use of wildcard for possible subdomains.
 | 
        
           |  |  | 209 |                 $escapeperiods = str_replace('.', '\.', $alloweddomain);
 | 
        
           |  |  | 210 |                 $replacewildcard = str_replace('*', '.*', $escapeperiods);
 | 
        
           |  |  | 211 |                 $ultimatepattern = '/' . $replacewildcard . '$/i';
 | 
        
           |  |  | 212 |                 if (preg_match($ultimatepattern, $domain)) {
 | 
        
           |  |  | 213 |                     return true;
 | 
        
           |  |  | 214 |                 }
 | 
        
           |  |  | 215 |             } else {
 | 
        
           |  |  | 216 |                 if (!self::is_domain_name($alloweddomain)) {
 | 
        
           |  |  | 217 |                     continue;
 | 
        
           |  |  | 218 |                 }
 | 
        
           |  |  | 219 |                 // Strict domain setting.
 | 
        
           |  |  | 220 |                 if (strcasecmp($domain, $alloweddomain) === 0) {
 | 
        
           |  |  | 221 |                     return true;
 | 
        
           |  |  | 222 |                 }
 | 
        
           |  |  | 223 |             }
 | 
        
           |  |  | 224 |         }
 | 
        
           |  |  | 225 |         return false;
 | 
        
           |  |  | 226 |     }
 | 
        
           |  |  | 227 |   | 
        
           |  |  | 228 |     /**
 | 
        
           |  |  | 229 |      * Is an ip in a given list of subnets?
 | 
        
           |  |  | 230 |      *
 | 
        
           |  |  | 231 |      * @param string $ip - the IP to test against the list
 | 
        
           |  |  | 232 |      * @param string $list - the list of IP subnets
 | 
        
           |  |  | 233 |      * @param string $delim a delimiter of the list
 | 
        
           |  |  | 234 |      * @return bool
 | 
        
           |  |  | 235 |      */
 | 
        
           |  |  | 236 |     public static function is_ip_in_subnet_list($ip, $list, $delim = "\n") {
 | 
        
           |  |  | 237 |         $list = explode($delim, $list);
 | 
        
           |  |  | 238 |         foreach ($list as $line) {
 | 
        
           |  |  | 239 |             $tokens = explode('#', $line);
 | 
        
           |  |  | 240 |             $subnet = trim($tokens[0]);
 | 
        
           |  |  | 241 |             if (address_in_subnet($ip, $subnet)) {
 | 
        
           |  |  | 242 |                 return true;
 | 
        
           |  |  | 243 |             }
 | 
        
           |  |  | 244 |         }
 | 
        
           |  |  | 245 |         return false;
 | 
        
           |  |  | 246 |     }
 | 
        
           |  |  | 247 |   | 
        
           |  |  | 248 |     /**
 | 
        
           |  |  | 249 |      * Return IP address for given hostname, or null on failure
 | 
        
           |  |  | 250 |      *
 | 
        
           |  |  | 251 |      * @param string $hostname
 | 
        
           |  |  | 252 |      * @return string|null
 | 
        
           |  |  | 253 |      */
 | 
        
           |  |  | 254 |     public static function get_ip_address(string $hostname): ?string {
 | 
        
           |  |  | 255 |         if (self::is_domain_name($hostname)) {
 | 
        
           |  |  | 256 |             $address = gethostbyname($hostname);
 | 
        
           |  |  | 257 |   | 
        
           |  |  | 258 |             // If address is different from hostname, we have success.
 | 
        
           |  |  | 259 |             if (strcasecmp($address, $hostname) !== 0) {
 | 
        
           |  |  | 260 |                 return $address;
 | 
        
           |  |  | 261 |             }
 | 
        
           |  |  | 262 |         }
 | 
        
           |  |  | 263 |   | 
        
           |  |  | 264 |         return null;
 | 
        
           |  |  | 265 |     }
 | 
        
           | 1441 | ariadna | 266 |   | 
        
           |  |  | 267 |     /**
 | 
        
           |  |  | 268 |      * Normalize internet address.
 | 
        
           |  |  | 269 |      *
 | 
        
           |  |  | 270 |      * Accepted input formats are :
 | 
        
           |  |  | 271 |      * - a valid range or full ip address (e.g.: 192.168.0.0/16, fe80::ffff, 127.0.0.1 or fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80)
 | 
        
           |  |  | 272 |      * - a valid domain name or pattern (e.g.: www.moodle.com or *.moodle.org)
 | 
        
           |  |  | 273 |      *
 | 
        
           |  |  | 274 |      * Convert forbidden syntaxes since MDL-74289 to allowed values. For examples:
 | 
        
           |  |  | 275 |      * - 192.168. => 192.168.0.0/16
 | 
        
           |  |  | 276 |      * - .domain.tld => *.domain.tld
 | 
        
           |  |  | 277 |      *
 | 
        
           |  |  | 278 |      * @param string $address The input string to normalize.
 | 
        
           |  |  | 279 |      *
 | 
        
           |  |  | 280 |      * @return string If $address is not normalizable, an empty string is returned.
 | 
        
           |  |  | 281 |      */
 | 
        
           |  |  | 282 |     public static function normalize_internet_address(string $address): string {
 | 
        
           |  |  | 283 |         $address = str_replace([" ", "\n", "\r", "\t", "\v", "\x00"], '', strtolower($address));
 | 
        
           |  |  | 284 |   | 
        
           |  |  | 285 |         // Replace previous allowed "192.168." format to CIDR format (192.168.0.0/16).
 | 
        
           |  |  | 286 |         if (str_ends_with($address, '.') && preg_match('/^[0-9\.]+$/', $address) === 1) {
 | 
        
           |  |  | 287 |             $count = substr_count($address, '.');
 | 
        
           |  |  | 288 |   | 
        
           |  |  | 289 |             // Remove final dot.
 | 
        
           |  |  | 290 |             $address = substr($address, 0, -1);
 | 
        
           |  |  | 291 |   | 
        
           |  |  | 292 |             // Fill address with missing ".0".
 | 
        
           |  |  | 293 |             $address .= str_repeat('.0', 4 - $count);
 | 
        
           |  |  | 294 |   | 
        
           |  |  | 295 |             // Add subnet mask.
 | 
        
           |  |  | 296 |             $address .= '/' . ($count * 8);
 | 
        
           |  |  | 297 |         }
 | 
        
           |  |  | 298 |   | 
        
           |  |  | 299 |         if (self::is_ip_address($address) ||
 | 
        
           |  |  | 300 |             self::is_ipv4_range($address) || self::is_ipv6_range($address)) {
 | 
        
           |  |  | 301 |   | 
        
           |  |  | 302 |             // Keep full or range ip addresses.
 | 
        
           |  |  | 303 |             return $address;
 | 
        
           |  |  | 304 |         }
 | 
        
           |  |  | 305 |   | 
        
           |  |  | 306 |         // Replace previous allowed ".domain.tld" format to "*.domain.tld" format.
 | 
        
           |  |  | 307 |         if (str_starts_with($address, '.')) {
 | 
        
           |  |  | 308 |             $address = '*'.$address;
 | 
        
           |  |  | 309 |         }
 | 
        
           |  |  | 310 |   | 
        
           |  |  | 311 |         // Usually the trailing dot (null label) is omitted, but is valid if supplied. We'll just remove it and validate as normal.
 | 
        
           |  |  | 312 |         $address = rtrim($address, '.');
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 314 |         if (self::is_domain_name($address) || self::is_domain_matching_pattern($address)) {
 | 
        
           |  |  | 315 |             // Keep valid or pattern domain name.
 | 
        
           |  |  | 316 |             return $address;
 | 
        
           |  |  | 317 |         }
 | 
        
           |  |  | 318 |   | 
        
           |  |  | 319 |         // Return empty string for invalid values.
 | 
        
           |  |  | 320 |         return '';
 | 
        
           |  |  | 321 |     }
 | 
        
           |  |  | 322 |   | 
        
           |  |  | 323 |     /**
 | 
        
           |  |  | 324 |      * Normalize a list of internet addresses.
 | 
        
           |  |  | 325 |      *
 | 
        
           |  |  | 326 |      * This function will:
 | 
        
           |  |  | 327 |      * - normalize internet addresses {@see normalize_internet_address()}
 | 
        
           |  |  | 328 |      * - remove invalid values
 | 
        
           |  |  | 329 |      * - remove duplicate values
 | 
        
           |  |  | 330 |      *
 | 
        
           |  |  | 331 |      * @param string $addresslist A string representing a list of internet addresses separated by a common value.
 | 
        
           |  |  | 332 |      * @param string $separator A separator character used within the list string.
 | 
        
           |  |  | 333 |      *
 | 
        
           |  |  | 334 |      * @return string
 | 
        
           |  |  | 335 |      */
 | 
        
           |  |  | 336 |     public static function normalize_internet_address_list(string $addresslist, string $separator = ','): string {
 | 
        
           |  |  | 337 |         $addresses = [];
 | 
        
           |  |  | 338 |         foreach (explode($separator, $addresslist) as $value) {
 | 
        
           |  |  | 339 |             $address = self::normalize_internet_address($value);
 | 
        
           |  |  | 340 |   | 
        
           |  |  | 341 |             if (empty($address)) {
 | 
        
           |  |  | 342 |                 // Ignore invalid input.
 | 
        
           |  |  | 343 |                 continue;
 | 
        
           |  |  | 344 |             }
 | 
        
           |  |  | 345 |   | 
        
           |  |  | 346 |             if (in_array($address, $addresses, true)) {
 | 
        
           |  |  | 347 |                 // Ignore duplicate value.
 | 
        
           |  |  | 348 |                 continue;
 | 
        
           |  |  | 349 |             }
 | 
        
           |  |  | 350 |   | 
        
           |  |  | 351 |             $addresses[] = $address;
 | 
        
           |  |  | 352 |         }
 | 
        
           |  |  | 353 |   | 
        
           |  |  | 354 |         return implode($separator, $addresses);
 | 
        
           |  |  | 355 |     }
 | 
        
           | 1 | efrain | 356 | }
 |