PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-ip / src / class-utils.php
jetpack / jetpack_vendor / automattic / jetpack-ip / src Last commit date
class-utils.php 1 month ago
class-utils.php
646 lines
1 <?php
2 /**
3 * Utils class file.
4 *
5 * @package automattic/jetpack-ip
6 */
7
8 namespace Automattic\Jetpack\IP;
9
10 /**
11 * Class that provides static methods for working with IP addresses.
12 */
13 class Utils {
14
15 const PACKAGE_VERSION = '0.5.0';
16
17 /**
18 * Get the current user's IP address.
19 *
20 * @return string|false IP address.
21 */
22 public static function get_ip() {
23 $trusted_header_data = get_site_option( 'trusted_ip_header' );
24 if ( isset( $trusted_header_data->trusted_header ) && isset( $_SERVER[ $trusted_header_data->trusted_header ] ) ) {
25 $ip = wp_unslash( $_SERVER[ $trusted_header_data->trusted_header ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip does it below.
26 $segments = $trusted_header_data->segments;
27 $reverse_order = $trusted_header_data->reverse;
28 } else {
29 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip does it below.
30 }
31
32 if ( ! $ip ) {
33 return false;
34 }
35
36 $ips = explode( ',', $ip );
37 if ( ! isset( $segments ) || ! $segments ) {
38 $segments = 1;
39 }
40 if ( isset( $reverse_order ) && $reverse_order ) {
41 $ips = array_reverse( $ips );
42 }
43 $ip_count = count( $ips );
44 if ( 1 === $ip_count ) {
45 return self::clean_ip( $ips[0] );
46 } elseif ( $ip_count >= $segments ) {
47 $the_one = $ip_count - $segments;
48 return self::clean_ip( $ips[ $the_one ] );
49 } else {
50 return self::clean_ip( isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip does it.
51 }
52 }
53
54 /**
55 * Clean IP address.
56 *
57 * @param string $ip The IP address to clean.
58 * @return string|false The cleaned IP address.
59 */
60 public static function clean_ip( $ip ) {
61
62 // Some misconfigured servers give back extra info, which comes after "unless".
63 $ips = explode( ' unless ', $ip );
64 $ip = $ips[0];
65
66 $ip = strtolower( trim( $ip ) );
67
68 // Check for IPv4 with port.
69 if ( preg_match( '/^(\d+\.\d+\.\d+\.\d+):\d+$/', $ip, $matches ) ) {
70 $ip = $matches[1];
71 }
72
73 // Check for IPv6 (or IPvFuture) with brackets and optional port.
74 if ( preg_match( '/^\[([a-z0-9\-._~!$&\'()*+,;=:]+)\](?::\d+)?$/', $ip, $matches ) ) {
75 $ip = $matches[1];
76 }
77
78 // Check for IPv4 IP cast as IPv6.
79 if ( preg_match( '/^::ffff:(\d+\.\d+\.\d+\.\d+)$/', $ip, $matches ) ) {
80 $ip = $matches[1];
81 }
82
83 // Validate and return.
84 return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : false;
85 }
86
87 /**
88 * Checks an IP to see if it is within a private range.
89 *
90 * @param string $ip IP address.
91 * @return bool True if IP address is private, false otherwise.
92 */
93 public static function ip_is_private( $ip ) {
94 // We are dealing with ipv6, so we can simply rely on filter_var.
95 // Note: str_contains() is not used here, as wp-includes/compat.php may not be loaded in this file.
96 if ( false === strpos( $ip, '.' ) ) {
97 return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
98 }
99 // We are dealing with ipv4.
100 $private_ip4_addresses = array(
101 '10.0.0.0|10.255.255.255', // Single class A network.
102 '172.16.0.0|172.31.255.255', // 16 contiguous class B network.
103 '192.168.0.0|192.168.255.255', // 256 contiguous class C network.
104 '169.254.0.0|169.254.255.255', // Link-local address also referred to as Automatic Private IP Addressing.
105 '127.0.0.0|127.255.255.255', // localhost.
106 );
107 $long_ip = ip2long( $ip );
108 if ( -1 !== $long_ip ) {
109 foreach ( $private_ip4_addresses as $pri_addr ) {
110 list ( $start, $end ) = explode( '|', $pri_addr );
111 if ( $long_ip >= ip2long( $start ) && $long_ip <= ip2long( $end ) ) {
112 return true;
113 }
114 }
115 }
116 return false;
117 }
118
119 /**
120 * Checks whether an IP address is a public, globally-routable destination.
121 *
122 * Stricter than the inverse of ip_is_private(): on top of private and reserved
123 * ranges it also rejects the IPv4 special-use ranges PHP's reserved-range
124 * filter leaves open (CGNAT, IETF protocol assignments, 6to4 relay anycast,
125 * benchmarking, multicast), the Azure metadata "Wire Server" address, and IPv6
126 * link-local / unique-local / site-local ranges. IPv6 addresses that embed an
127 * IPv4 address (IPv4-mapped ::ffff:0:0/96, IPv4-compatible ::/96, NAT64
128 * 64:ff9b::/96, and 6to4 2002::/16) are decoded to that IPv4 and re-checked, so
129 * the embedded IPv4 is classified the same way whether or not it is wrapped.
130 *
131 * @param string $ip IP address (IPv4, IPv6, or IPv4-mapped IPv6; any IPv6 zone id is ignored).
132 * @return bool True when the address is a safe public destination, false otherwise.
133 */
134 public static function ip_is_public( $ip ) {
135 if ( ! is_string( $ip ) || '' === $ip ) {
136 return false;
137 }
138
139 // Strip an IPv6 zone identifier (e.g. fe80::1%eth0). Zone ids are only valid
140 // on IPv6 addresses, so a '%' on anything else (e.g. "8.8.8.8%foo") is malformed.
141 if ( false !== strpos( $ip, '%' ) ) {
142 if ( false === strpos( $ip, ':' ) ) {
143 return false;
144 }
145 $ip = preg_replace( '/%.*$/', '', $ip );
146 }
147
148 // Decode IPv6 forms that embed an IPv4 address to that IPv4 and check it,
149 // so the embedded IPv4 is classified the same way whether or not it is wrapped.
150 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
151 $binary = inet_pton( $ip );
152 if ( false !== $binary && 16 === strlen( $binary ) ) {
153 $prefix12 = substr( $binary, 0, 12 );
154 $embedded = null;
155
156 if (
157 "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" === $prefix12 // IPv4-mapped ::ffff:0:0/96.
158 || "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" === $prefix12 // IPv4-compatible ::/96 (incl. ::, ::1).
159 || "\x00\x64\xff\x9b\x00\x00\x00\x00\x00\x00\x00\x00" === $prefix12 // NAT64 64:ff9b::/96.
160 ) {
161 $embedded = substr( $binary, 12, 4 );
162 } elseif ( "\x20\x02" === substr( $binary, 0, 2 ) ) {
163 // 6to4 2002::/16: the embedded IPv4 gateway is in bytes 2-5.
164 $embedded = substr( $binary, 2, 4 );
165 }
166
167 if ( null !== $embedded ) {
168 $mapped = inet_ntop( $embedded );
169 if ( is_string( $mapped ) ) {
170 $ip = $mapped;
171 }
172 }
173 }
174 }
175
176 // Reject anything that is not a valid, non-private, non-reserved address.
177 if ( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
178 return false;
179 }
180
181 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
182 // IPv4 special-use ranges the reserved-range filter above leaves open.
183 $blocked_ranges = array(
184 array( '100.64.0.0', '100.127.255.255' ), // CGNAT (RFC 6598).
185 array( '168.63.129.16', '168.63.129.16' ), // Azure metadata "Wire Server".
186 array( '169.254.0.0', '169.254.255.255' ), // Link-local, incl. cloud metadata.
187 array( '192.0.0.0', '192.0.0.255' ), // IETF protocol assignments (RFC 6890).
188 array( '192.88.99.0', '192.88.99.255' ), // 6to4 relay anycast (RFC 7526).
189 array( '198.18.0.0', '198.19.255.255' ), // Benchmarking (RFC 2544).
190 array( '224.0.0.0', '239.255.255.255' ), // Multicast (RFC 5771).
191 );
192 foreach ( $blocked_ranges as $range ) {
193 if ( self::ip_address_is_in_range( $ip, $range[0], $range[1] ) ) {
194 return false;
195 }
196 }
197 return true;
198 }
199
200 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
201 $binary = inet_pton( $ip );
202 if ( false === $binary || strlen( $binary ) < 2 ) {
203 return false;
204 }
205 $first = unpack( 'n', $binary )[1];
206
207 // fe80::/10 link-local and fec0::/10 site-local (deprecated).
208 if ( 0xfe80 === ( $first & 0xffc0 ) || 0xfec0 === ( $first & 0xffc0 ) ) {
209 return false;
210 }
211 // fc00::/7 unique local addresses.
212 if ( 0xfc00 === ( $first & 0xfe00 ) ) {
213 return false;
214 }
215 return true;
216 }
217
218 return false;
219 }
220
221 /**
222 * Validate an IP address.
223 *
224 * @param string $ip IP address.
225 * @return bool True if valid, false otherwise.
226 */
227 private static function validate_ip_address( string $ip ) {
228 return filter_var( $ip, FILTER_VALIDATE_IP );
229 }
230
231 /**
232 * Validate an array of IP addresses.
233 *
234 * @param array $ips List of IP addresses.
235 * @return bool True if all IPs are valid, false otherwise.
236 */
237 private static function validate_ip_addresses( array $ips ) {
238 foreach ( $ips as $ip ) {
239 if ( ! self::validate_ip_address( $ip ) ) {
240 return false;
241 }
242 }
243 return true;
244 }
245
246 /**
247 * Uses inet_pton if available to convert an IP address to a binary string.
248 * Returns false if an invalid IP address is given.
249 *
250 * @param mixed $ip IP address.
251 * @return int|string|bool
252 */
253 public static function convert_ip_address( $ip ) {
254 return inet_pton( $ip );
255 }
256
257 /**
258 * Determines the IP version of the given IP address.
259 *
260 * @param string $ip IP address.
261 * @return string|false 'ipv4', 'ipv6', or false if invalid.
262 */
263 public static function get_ip_version( $ip ) {
264 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
265 return 'ipv4';
266 } elseif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
267 return 'ipv6';
268 } else {
269 return false;
270 }
271 }
272
273 /**
274 * Extracts IP addresses from a given string.
275 *
276 * Supports IPv4 and IPv6 ranges in both hyphen and CIDR notation.
277 *
278 * @param string $ips List of IPs.
279 * @return array List of valid IP addresses or ranges.
280 */
281 public static function get_ip_addresses_from_string( $ips ) {
282 // Split the string by spaces, commas, and semicolons.
283 $ips = preg_split( '/[\s,;]/', (string) $ips );
284
285 $result = array();
286
287 foreach ( $ips as $ip ) {
288 $ip = trim( $ip );
289
290 // Check for CIDR notation
291 if ( strpos( $ip, '/' ) !== false ) {
292 if ( self::validate_cidr( $ip ) ) {
293 $result[] = $ip;
294 }
295 continue;
296 }
297
298 // Validate both IP values from the hyphen range.
299 $range = explode( '-', $ip );
300 if ( count( $range ) === 2 ) {
301 if ( self::validate_ip_range( $range[0], $range[1] ) ) {
302 $result[] = $ip;
303 }
304 continue;
305 }
306
307 // Validate the single IP value.
308 if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
309 $result[] = $ip;
310 }
311 }
312
313 return $result;
314 }
315
316 /**
317 * Validates CIDR notation for IPv4 and IPv6 addresses.
318 *
319 * @param string $cidr CIDR notation IP address.
320 * @return bool True if valid, false otherwise.
321 */
322 public static function validate_cidr( $cidr ) {
323 // Split the CIDR notation into IP address and prefix length using the '/' separator.
324 $parts = explode( '/', $cidr );
325 if ( count( $parts ) !== 2 ) {
326 return false; // Invalid CIDR notation if it doesn't contain exactly one '/'.
327 }
328
329 list( $ip, $netmask ) = $parts;
330
331 // Validate the IP address.
332 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
333 return false;
334 }
335
336 $ip_version = self::get_ip_version( $ip );
337 if ( ! $ip_version ) {
338 return false; // Invalid IP address.
339 }
340
341 // Validate the netmask based on the IP version.
342 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
343 return false;
344 }
345
346 return true;
347 }
348
349 /**
350 * Checks if an IP address is within a CIDR range.
351 * Supports both IPv4 and IPv6.
352 *
353 * @param string $ip IP address.
354 * @param string $cidr CIDR notation IP range.
355 * @return bool True if IP is within the range, false otherwise.
356 */
357 public static function ip_in_cidr( $ip, $cidr ) {
358 // Parse the CIDR notation to extract the base IP address and netmask prefix length.
359 $parsed_cidr = self::parse_cidr( $cidr );
360 if ( ! $parsed_cidr ) {
361 return false;
362 }
363 list( $range, $netmask ) = $parsed_cidr;
364
365 // Determine the IP version (IPv4 or IPv6) of both the input IP and the CIDR range IP.
366 $ip_version = self::get_ip_version( $ip );
367 $range_version = self::get_ip_version( $range );
368
369 // Ensure both IP addresses are valid and of the same IP version.
370 if ( ! $ip_version || ! $range_version || $ip_version !== $range_version ) {
371 return false;
372 }
373
374 // Validate the netmask based on the IP version.
375 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
376 return false;
377 }
378
379 if ( $ip_version === 'ipv4' ) {
380 return self::ip_in_ipv4_cidr( $ip, $range, $netmask );
381 } else {
382 return self::ip_in_ipv6_cidr( $ip, $range, $netmask );
383 }
384 }
385
386 /**
387 * Parses the CIDR notation into network address and netmask.
388 *
389 * @param string $cidr CIDR notation IP range.
390 * @return array|false Array containing network address and netmask, or false on failure.
391 */
392 public static function parse_cidr( $cidr ) {
393 $cidr_parts = explode( '/', $cidr, 2 );
394 if ( count( $cidr_parts ) !== 2 ) {
395 return false; // Invalid CIDR notation
396 }
397 list( $range, $netmask ) = $cidr_parts;
398
399 // Determine IP version
400 $ip_version = self::get_ip_version( $range );
401 if ( ! $ip_version ) {
402 return false; // Invalid IP address
403 }
404
405 // Validate netmask range
406 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
407 return false; // Netmask out of range
408 }
409
410 return array( $range, (int) $netmask );
411 }
412
413 /**
414 * Validates the netmask based on IP version.
415 *
416 * @param string|int $netmask Netmask value.
417 * @param string $ip_version 'ipv4' or 'ipv6'.
418 * @return bool True if valid, false otherwise.
419 */
420 public static function validate_netmask( $netmask, $ip_version ) {
421 // Ensure that $netmask is an integer
422 if ( ! ctype_digit( (string) $netmask ) ) {
423 return false;
424 }
425 $netmask = (int) $netmask;
426
427 // Validate the netmask based on the IP version.
428 if ( $ip_version === 'ipv4' ) {
429 return ( $netmask >= 0 && $netmask <= 32 );
430 } elseif ( $ip_version === 'ipv6' ) {
431 return ( $netmask >= 0 && $netmask <= 128 );
432 } else {
433 return false;
434 }
435 }
436
437 /**
438 * Checks if an IPv4 address is within a CIDR range.
439 *
440 * @param string $ip IPv4 address to check.
441 * @param string $range IPv4 network address.
442 * @param int $netmask Netmask value.
443 * @return bool True if IP is within the range, false otherwise.
444 */
445 public static function ip_in_ipv4_cidr( $ip, $range, $netmask ) {
446 // Validate arguments.
447 if ( ! self::validate_ip_addresses( array( $ip, $range ) ) || ! self::validate_netmask( $netmask, 'ipv4' ) ) {
448 return false; // Invalid IP address or netmask.
449 }
450
451 // Convert IP addresses from their dotted representation to 32-bit unsigned integers.
452 $ip_long = ip2long( $ip );
453 $range_long = ip2long( $range );
454
455 // Check if the conversion was successful.
456 if ( $ip_long === false || $range_long === false ) {
457 return false; // One of the IP addresses is invalid.
458 }
459
460 /**
461 * Create the subnet mask as a 32-bit unsigned integer.
462 *
463 * Explanation:
464 * - (32 - $netmask) calculates the number of host bits (the bits not used for the network address).
465 * - (1 << (32 - $netmask)) shifts the number 1 left by the number of host bits.
466 * This results in a number where there is a single 1 followed by zeros equal to the number of host bits.
467 * - Subtracting 1 gives us a number where the host bits are all 1s.
468 * - Applying the bitwise NOT operator (~) inverts the bits, turning all host bits to 0 and network bits to 1.
469 * This results in the subnet mask having 1s in the network portion and 0s in the host portion.
470 *
471 * Example for netmask = 24:
472 * - (32 - 24) = 8
473 * - (1 << 8) = 256 (binary: 00000000 00000000 00000001 00000000)
474 * - 256 - 1 = 255 (binary: 00000000 00000000 00000000 11111111)
475 * - ~255 = 4294967040 (binary: 11111111 11111111 11111111 00000000)
476 */
477 $mask = ~ ( ( 1 << ( 32 - $netmask ) ) - 1 );
478
479 /**
480 * Use bitwise AND to apply the subnet mask to both the IP address and the network address.
481 * - ($ip_long & $mask) isolates the network portion of the IP address.
482 * - ($range_long & $mask) isolates the network portion of the CIDR range.
483 * - If both network portions are equal, the IP address belongs to the same subnet and is within the CIDR range.
484 */
485 return ( $ip_long & $mask ) === ( $range_long & $mask );
486 }
487
488 /**
489 * Checks if an IPv6 address is within a CIDR range.
490 *
491 * @param string $ip IPv6 address to check.
492 * @param string $range IPv6 network address.
493 * @param int $netmask Netmask value.
494 * @return bool True if IP is within the range, false otherwise.
495 */
496 public static function ip_in_ipv6_cidr( $ip, $range, $netmask ) {
497 // Validate arguments.
498 if ( ! self::validate_ip_addresses( array( $ip, $range ) ) || ! self::validate_netmask( $netmask, 'ipv6' ) ) {
499 return false; // Invalid IP address or netmask.
500 }
501
502 // Convert IP addresses from their textual representation to binary strings.
503 $ip_bin = inet_pton( $ip );
504 $range_bin = inet_pton( $range );
505
506 // Check if the conversion was successful.
507 if ( $ip_bin === false || $range_bin === false ) {
508 return false; // One of the IP addresses is invalid.
509 }
510
511 /**
512 * Calculate the subnet mask in binary form.
513 *
514 * IPv6 addresses are 128 bits long.
515 * The netmask defines how many bits are set to 1 in the subnet mask.
516 *
517 * - $netmask_full_bytes: Number of full bytes (each 8 bits) that are all 1s.
518 * - $netmask_remainder_bits: Remaining bits (less than 8) that need to be set to 1.
519 *
520 * For example, if $netmask = 65:
521 * - $netmask_full_bytes = floor(65 / 8) = 8 (since 8 * 8 = 64 bits)
522 * - $netmask_remainder_bits = 65 % 8 = 1 (1 bit remaining)
523 *
524 * We'll construct the subnet mask by:
525 * - Starting with $netmask_full_bytes of 0xff (11111111 in binary).
526 * - Adding a byte where the first $netmask_remainder_bits bits are 1, rest are 0.
527 * - Padding the rest with zeros to make it 16 bytes (128 bits) long.
528 */
529
530 // Number of full bytes (each full byte is 8 bits) in the netmask.
531 $netmask_full_bytes = (int) ( $netmask / 8 );
532
533 // Number of remaining bits in the last byte of the netmask.
534 $netmask_remainder_bits = $netmask % 8;
535
536 // Start with a string of $netmask_full_bytes of 0xff bytes (each byte is 8 bits set to 1).
537 $netmask_bin = str_repeat( "\xff", $netmask_full_bytes );
538
539 if ( $netmask_remainder_bits > 0 ) {
540 // Create the last byte with $netmask_remainder_bits bits set to 1 from the left.
541 // - str_repeat('1', $netmask_remainder_bits): creates a string with the required number of '1's.
542 // - str_pad(...): pads the string on the right with '0's to make it 8 bits.
543 // - bindec(...): converts the binary string to a decimal number.
544 // - chr(...): gets the character corresponding to the byte value.
545 $last_byte = chr( bindec( str_pad( str_repeat( '1', $netmask_remainder_bits ), 8, '0', STR_PAD_RIGHT ) ) );
546 // Append the last byte to the netmask binary string.
547 $netmask_bin .= $last_byte;
548 }
549
550 // Pad the netmask binary string to 16 bytes (128 bits) with zeros (\x00).
551 $netmask_bin = str_pad( $netmask_bin, 16, "\x00" );
552
553 /**
554 * Use bitwise AND to apply the subnet mask to both the IP address and the network address.
555 * - ($ip_bin & $netmask_bin) isolates the network portion of the IP address.
556 * - ($range_bin & $netmask_bin) isolates the network portion of the CIDR range.
557 * - If both network portions are equal, the IP address belongs to the same subnet and is within the CIDR range.
558 */
559 return ( $ip_bin & $netmask_bin ) === ( $range_bin & $netmask_bin );
560 }
561
562 /**
563 * Validates the low and high IP addresses of a range.
564 *
565 * Now supports IPv6 addresses.
566 *
567 * @param string $range_low Low IP address.
568 * @param string $range_high High IP address.
569 * @return bool True if the range is valid, false otherwise.
570 */
571 public static function validate_ip_range( $range_low, $range_high ) {
572 // Validate that both IP addresses are valid.
573 if ( self::validate_ip_addresses( array( $range_low, $range_high ) ) === false ) {
574 return false;
575 }
576
577 // Ensure both IPs are of the same version
578 $range_low_ip_version = self::get_ip_version( $range_low );
579 $range_high_ip_version = self::get_ip_version( $range_high );
580
581 if ( $range_low_ip_version !== $range_high_ip_version || ! $range_low_ip_version || ! $range_high_ip_version ) {
582 return false; // Invalid or mixed IP versions.
583 }
584
585 // Convert IP addresses to their packed binary representation.
586 $ip_low = inet_pton( $range_low );
587 $ip_high = inet_pton( $range_high );
588
589 // Check if the conversion was successful.
590 if ( false === $ip_low || false === $ip_high ) {
591 return false;
592 }
593
594 // Compare the binary representations to ensure the low IP is not greater than the high IP.
595 if ( strcmp( $ip_low, $ip_high ) > 0 ) {
596 return false;
597 }
598
599 return true;
600 }
601
602 /**
603 * Checks that a given IP address is within a given range.
604 *
605 * Supports CIDR notation and hyphenated ranges for both IPv4 and IPv6.
606 *
607 * @param string $ip IP address.
608 * @param string $range_low Range low or CIDR notation.
609 * @param null|string $range_high Optional. Range high. Not used if $range_low is CIDR notation.
610 * @return bool
611 */
612 public static function ip_address_is_in_range( $ip, $range_low, $range_high = null ) {
613 // Validate that all provided IP addresses are valid.
614 if ( $range_high !== null && ! self::validate_ip_addresses( array( $ip, $range_low, $range_high ) ) ) {
615 return false;
616 } else {
617 $range_low_parsed = self::parse_cidr( $range_low );
618 if ( $range_low_parsed && ! self::validate_ip_addresses( array( $ip, $range_low_parsed[0] ) ) ) {
619 return false;
620 }
621 }
622
623 if ( strpos( $range_low, '/' ) !== false ) {
624 // CIDR notation
625 if ( $range_high !== null ) {
626 // Invalid usage: CIDR notation with range high parameter
627 return false;
628 }
629 return self::ip_in_cidr( $ip, $range_low );
630 }
631
632 // Hyphenated range
633 if ( $range_high === null ) {
634 return false; // Invalid parameters
635 }
636
637 $ip_num = inet_pton( $ip );
638 $ip_low = inet_pton( $range_low );
639 $ip_high = inet_pton( $range_high );
640 if ( $ip_num && $ip_low && $ip_high && strcmp( $ip_num, $ip_low ) >= 0 && strcmp( $ip_num, $ip_high ) <= 0 ) {
641 return true;
642 }
643 return false;
644 }
645 }
646