Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/libs/IP.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Functions and constants to play with IP addresses and ranges
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author Antoine Musso "<hashar at free dot fr>", Aaron Schulz
22
 */
23
24
use IPSet\IPSet;
0 ignored issues
show
This use statement conflicts with another class in this namespace, IPSet.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
25
26
// Some regex definition to "play" with IP address and IP address blocks
27
28
// An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255
29
define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' );
30
define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE );
31
// An IPv4 block is an IP address and a prefix (d1 to d32)
32
define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' );
33
define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX );
34
35
// An IPv6 address is made up of 8 words (each x0000 to xFFFF).
36
// However, the "::" abbreviation can be used on consecutive x0000 words.
37
define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
38
define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' );
39
define( 'RE_IPV6_ADD',
40
	'(?:' . // starts with "::" (including "::")
41
		':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
42
	'|' . // ends with "::" (except "::")
43
		RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
44
	'|' . // contains one "::" in the middle (the ^ makes the test fail if none found)
45
		RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
46
	'|' . // contains no "::"
47
		RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
48
	')'
49
);
50
// An IPv6 block is an IP address and a prefix (d1 to d128)
51
define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
52
// For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
53
define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
54
define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
55
56
// This might be useful for regexps used elsewhere, matches any IPv4 or IPv6 address or network
57
define( 'IP_ADDRESS_STRING',
58
	'(?:' .
59
		RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4
60
	'|' .
61
		RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6
62
	')'
63
);
64
65
/**
66
 * A collection of public static functions to play with IP address
67
 * and IP blocks.
68
 */
69
class IP {
70
	/** @var IPSet */
71
	private static $proxyIpSet = null;
0 ignored issues
show
The property $proxyIpSet is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
72
73
	/**
74
	 * Determine if a string is as valid IP address or network (CIDR prefix).
75
	 * SIIT IPv4-translated addresses are rejected.
76
	 * @note canonicalize() tries to convert translated addresses to IPv4.
77
	 *
78
	 * @param string $ip Possible IP address
79
	 * @return bool
80
	 */
81
	public static function isIPAddress( $ip ) {
82
		return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip );
83
	}
84
85
	/**
86
	 * Given a string, determine if it as valid IP in IPv6 only.
87
	 * @note Unlike isValid(), this looks for networks too.
88
	 *
89
	 * @param string $ip Possible IP address
90
	 * @return bool
91
	 */
92
	public static function isIPv6( $ip ) {
93
		return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip );
94
	}
95
96
	/**
97
	 * Given a string, determine if it as valid IP in IPv4 only.
98
	 * @note Unlike isValid(), this looks for networks too.
99
	 *
100
	 * @param string $ip Possible IP address
101
	 * @return bool
102
	 */
103
	public static function isIPv4( $ip ) {
104
		return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip );
105
	}
106
107
	/**
108
	 * Validate an IP address. Ranges are NOT considered valid.
109
	 * SIIT IPv4-translated addresses are rejected.
110
	 * @note canonicalize() tries to convert translated addresses to IPv4.
111
	 *
112
	 * @param string $ip
113
	 * @return bool True if it is valid
114
	 */
115
	public static function isValid( $ip ) {
116
		return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip )
117
			|| preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) );
118
	}
119
120
	/**
121
	 * Validate an IP Block (valid address WITH a valid prefix).
122
	 * SIIT IPv4-translated addresses are rejected.
123
	 * @note canonicalize() tries to convert translated addresses to IPv4.
124
	 *
125
	 * @param string $ipblock
126
	 * @return bool True if it is valid
127
	 */
128
	public static function isValidBlock( $ipblock ) {
129
		return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock )
130
			|| preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) );
131
	}
132
133
	/**
134
	 * Convert an IP into a verbose, uppercase, normalized form.
135
	 * Both IPv4 and IPv6 addresses are trimmed. Additionally,
136
	 * IPv6 addresses in octet notation are expanded to 8 words;
137
	 * IPv4 addresses have leading zeros, in each octet, removed.
138
	 *
139
	 * @param string $ip IP address in quad or octet form (CIDR or not).
140
	 * @return string
141
	 */
142
	public static function sanitizeIP( $ip ) {
143
		$ip = trim( $ip );
144
		if ( $ip === '' ) {
145
			return null;
146
		}
147
		/* If not an IP, just return trimmed value, since sanitizeIP() is called
148
		 * in a number of contexts where usernames are supplied as input.
149
		 */
150
		if ( !self::isIPAddress( $ip ) ) {
151
			return $ip;
152
		}
153
		if ( self::isIPv4( $ip ) ) {
154
			// Remove leading 0's from octet representation of IPv4 address
155
			$ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip );
156
			return $ip;
157
		}
158
		// Remove any whitespaces, convert to upper case
159
		$ip = strtoupper( $ip );
160
		// Expand zero abbreviations
161
		$abbrevPos = strpos( $ip, '::' );
162
		if ( $abbrevPos !== false ) {
163
			// We know this is valid IPv6. Find the last index of the
164
			// address before any CIDR number (e.g. "a:b:c::/24").
165
			$CIDRStart = strpos( $ip, "/" );
166
			$addressEnd = ( $CIDRStart !== false )
167
				? $CIDRStart - 1
168
				: strlen( $ip ) - 1;
169
			// If the '::' is at the beginning...
170
			if ( $abbrevPos == 0 ) {
171
				$repeat = '0:';
172
				$extra = ( $ip == '::' ) ? '0' : ''; // for the address '::'
173
				$pad = 9; // 7+2 (due to '::')
174
			// If the '::' is at the end...
175
			} elseif ( $abbrevPos == ( $addressEnd - 1 ) ) {
176
				$repeat = ':0';
177
				$extra = '';
178
				$pad = 9; // 7+2 (due to '::')
179
			// If the '::' is in the middle...
180
			} else {
181
				$repeat = ':0';
182
				$extra = ':';
183
				$pad = 8; // 6+2 (due to '::')
184
			}
185
			$ip = str_replace( '::',
186
				str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra,
187
				$ip
188
			);
189
		}
190
		// Remove leading zeros from each bloc as needed
191
		$ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
192
193
		return $ip;
194
	}
195
196
	/**
197
	 * Prettify an IP for display to end users.
198
	 * This will make it more compact and lower-case.
199
	 *
200
	 * @param string $ip
201
	 * @return string
202
	 */
203
	public static function prettifyIP( $ip ) {
204
		$ip = self::sanitizeIP( $ip ); // normalize (removes '::')
205
		if ( self::isIPv6( $ip ) ) {
206
			// Split IP into an address and a CIDR
207
			if ( strpos( $ip, '/' ) !== false ) {
208
				list( $ip, $cidr ) = explode( '/', $ip, 2 );
209
			} else {
210
				list( $ip, $cidr ) = [ $ip, '' ];
211
			}
212
			// Get the largest slice of words with multiple zeros
213
			$offset = 0;
214
			$longest = $longestPos = false;
215
			while ( preg_match(
216
				'!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset
217
			) ) {
218
				list( $match, $pos ) = $m[0]; // full match
219
				if ( strlen( $match ) > strlen( $longest ) ) {
220
					$longest = $match;
221
					$longestPos = $pos;
222
				}
223
				$offset = ( $pos + strlen( $match ) ); // advance
224
			}
225
			if ( $longest !== false ) {
226
				// Replace this portion of the string with the '::' abbreviation
227
				$ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) );
228
			}
229
			// Add any CIDR back on
230
			if ( $cidr !== '' ) {
231
				$ip = "{$ip}/{$cidr}";
232
			}
233
			// Convert to lower case to make it more readable
234
			$ip = strtolower( $ip );
235
		}
236
237
		return $ip;
238
	}
239
240
	/**
241
	 * Given a host/port string, like one might find in the host part of a URL
242
	 * per RFC 2732, split the hostname part and the port part and return an
243
	 * array with an element for each. If there is no port part, the array will
244
	 * have false in place of the port. If the string was invalid in some way,
245
	 * false is returned.
246
	 *
247
	 * This was easy with IPv4 and was generally done in an ad-hoc way, but
248
	 * with IPv6 it's somewhat more complicated due to the need to parse the
249
	 * square brackets and colons.
250
	 *
251
	 * A bare IPv6 address is accepted despite the lack of square brackets.
252
	 *
253
	 * @param string $both The string with the host and port
254
	 * @return array|false Array normally, false on certain failures
255
	 */
256
	public static function splitHostAndPort( $both ) {
257
		if ( substr( $both, 0, 1 ) === '[' ) {
258
			if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) {
259
				if ( isset( $m['port'] ) ) {
260
					return [ $m[1], intval( $m['port'] ) ];
261
				} else {
262
					return [ $m[1], false ];
263
				}
264
			} else {
265
				// Square bracket found but no IPv6
266
				return false;
267
			}
268
		}
269
		$numColons = substr_count( $both, ':' );
270
		if ( $numColons >= 2 ) {
271
			// Is it a bare IPv6 address?
272
			if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
273
				return [ $both, false ];
274
			} else {
275
				// Not valid IPv6, but too many colons for anything else
276
				return false;
277
			}
278
		}
279
		if ( $numColons >= 1 ) {
280
			// Host:port?
281
			$bits = explode( ':', $both );
282
			if ( preg_match( '/^\d+/', $bits[1] ) ) {
283
				return [ $bits[0], intval( $bits[1] ) ];
284
			} else {
285
				// Not a valid port
286
				return false;
287
			}
288
		}
289
290
		// Plain hostname
291
		return [ $both, false ];
292
	}
293
294
	/**
295
	 * Given a host name and a port, combine them into host/port string like
296
	 * you might find in a URL. If the host contains a colon, wrap it in square
297
	 * brackets like in RFC 2732. If the port matches the default port, omit
298
	 * the port specification
299
	 *
300
	 * @param string $host
301
	 * @param int $port
302
	 * @param bool|int $defaultPort
303
	 * @return string
304
	 */
305
	public static function combineHostAndPort( $host, $port, $defaultPort = false ) {
306
		if ( strpos( $host, ':' ) !== false ) {
307
			$host = "[$host]";
308
		}
309
		if ( $defaultPort !== false && $port == $defaultPort ) {
310
			return $host;
311
		} else {
312
			return "$host:$port";
313
		}
314
	}
315
316
	/**
317
	 * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
318
	 *
319
	 * @param string $hex Number, with "v6-" prefix if it is IPv6
320
	 * @return string Quad-dotted (IPv4) or octet notation (IPv6)
321
	 */
322
	public static function formatHex( $hex ) {
323
		if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6
324
			return self::hexToOctet( substr( $hex, 3 ) );
325
		} else { // IPv4
326
			return self::hexToQuad( $hex );
327
		}
328
	}
329
330
	/**
331
	 * Converts a hexadecimal number to an IPv6 address in octet notation
332
	 *
333
	 * @param string $ip_hex Pure hex (no v6- prefix)
334
	 * @return string (of format a:b:c:d:e:f:g:h)
335
	 */
336
	public static function hexToOctet( $ip_hex ) {
337
		// Pad hex to 32 chars (128 bits)
338
		$ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT );
339
		// Separate into 8 words
340
		$ip_oct = substr( $ip_hex, 0, 4 );
341
		for ( $n = 1; $n < 8; $n++ ) {
342
			$ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 );
343
		}
344
		// NO leading zeroes
345
		$ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct );
346
347
		return $ip_oct;
348
	}
349
350
	/**
351
	 * Converts a hexadecimal number to an IPv4 address in quad-dotted notation
352
	 *
353
	 * @param string $ip_hex Pure hex
354
	 * @return string (of format a.b.c.d)
355
	 */
356
	public static function hexToQuad( $ip_hex ) {
357
		// Pad hex to 8 chars (32 bits)
358
		$ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT );
359
		// Separate into four quads
360
		$s = '';
361
		for ( $i = 0; $i < 4; $i++ ) {
362
			if ( $s !== '' ) {
363
				$s .= '.';
364
			}
365
			$s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 );
366
		}
367
368
		return $s;
369
	}
370
371
	/**
372
	 * Determine if an IP address really is an IP address, and if it is public,
373
	 * i.e. not RFC 1918 or similar
374
	 *
375
	 * @param string $ip
376
	 * @return bool
377
	 */
378
	public static function isPublic( $ip ) {
379
		static $privateSet = null;
380
		if ( !$privateSet ) {
381
			$privateSet = new IPSet( [
382
				'10.0.0.0/8', # RFC 1918 (private)
383
				'172.16.0.0/12', # RFC 1918 (private)
384
				'192.168.0.0/16', # RFC 1918 (private)
385
				'0.0.0.0/8', # this network
386
				'127.0.0.0/8', # loopback
387
				'fc00::/7', # RFC 4193 (local)
388
				'0:0:0:0:0:0:0:1', # loopback
389
				'169.254.0.0/16', # link-local
390
				'fe80::/10', # link-local
391
			] );
392
		}
393
		return !$privateSet->match( $ip );
394
	}
395
396
	/**
397
	 * Return a zero-padded upper case hexadecimal representation of an IP address.
398
	 *
399
	 * Hexadecimal addresses are used because they can easily be extended to
400
	 * IPv6 support. To separate the ranges, the return value from this
401
	 * function for an IPv6 address will be prefixed with "v6-", a non-
402
	 * hexadecimal string which sorts after the IPv4 addresses.
403
	 *
404
	 * @param string $ip Quad dotted/octet IP address.
405
	 * @return string|bool False on failure
406
	 */
407
	public static function toHex( $ip ) {
408
		if ( self::isIPv6( $ip ) ) {
409
			$n = 'v6-' . self::IPv6ToRawHex( $ip );
410
		} elseif ( self::isIPv4( $ip ) ) {
411
			// T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
412
			// also double/triple 0 needs to be changed to just a single 0 for ip2long.
413
			$ip = self::sanitizeIP( $ip );
414
			$n = ip2long( $ip );
415
			if ( $n < 0 ) {
416
				$n += pow( 2, 32 );
417
				# On 32-bit platforms (and on Windows), 2^32 does not fit into an int,
418
				# so $n becomes a float. We convert it to string instead.
419
				if ( is_float( $n ) ) {
420
					$n = (string)$n;
421
				}
422
			}
423
			if ( $n !== false ) {
424
				# Floating points can handle the conversion; faster than Wikimedia\base_convert()
425
				$n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) );
426
			}
427
		} else {
428
			$n = false;
429
		}
430
431
		return $n;
432
	}
433
434
	/**
435
	 * Given an IPv6 address in octet notation, returns a pure hex string.
436
	 *
437
	 * @param string $ip Octet ipv6 IP address.
438
	 * @return string|bool Pure hex (uppercase); false on failure
439
	 */
440
	private static function IPv6ToRawHex( $ip ) {
441
		$ip = self::sanitizeIP( $ip );
442
		if ( !$ip ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ip of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
443
			return false;
444
		}
445
		$r_ip = '';
446
		foreach ( explode( ':', $ip ) as $v ) {
447
			$r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
448
		}
449
450
		return $r_ip;
451
	}
452
453
	/**
454
	 * Convert a network specification in CIDR notation
455
	 * to an integer network and a number of bits
456
	 *
457
	 * @param string $range IP with CIDR prefix
458
	 * @return array(int or string, int)
0 ignored issues
show
The doc-type array(int could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
459
	 */
460
	public static function parseCIDR( $range ) {
461
		if ( self::isIPv6( $range ) ) {
462
			return self::parseCIDR6( $range );
463
		}
464
		$parts = explode( '/', $range, 2 );
465
		if ( count( $parts ) != 2 ) {
466
			return [ false, false ];
467
		}
468
		list( $network, $bits ) = $parts;
469
		$network = ip2long( $network );
470
		if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) {
471
			if ( $bits == 0 ) {
472
				$network = 0;
473
			} else {
474
				$network &= ~( ( 1 << ( 32 - $bits ) ) - 1 );
475
			}
476
			# Convert to unsigned
477
			if ( $network < 0 ) {
478
				$network += pow( 2, 32 );
479
			}
480
		} else {
481
			$network = false;
482
			$bits = false;
483
		}
484
485
		return [ $network, $bits ];
486
	}
487
488
	/**
489
	 * Given a string range in a number of formats,
490
	 * return the start and end of the range in hexadecimal.
491
	 *
492
	 * Formats are:
493
	 *     1.2.3.4/24          CIDR
494
	 *     1.2.3.4 - 1.2.3.5   Explicit range
495
	 *     1.2.3.4             Single IP
496
	 *
497
	 *     2001:0db8:85a3::7344/96                       CIDR
498
	 *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
499
	 *     2001:0db8:85a3::7344                          Single IP
500
	 * @param string $range IP range
501
	 * @return array(string, string)
0 ignored issues
show
The doc-type array(string, could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
502
	 */
503
	public static function parseRange( $range ) {
504
		// CIDR notation
505
		if ( strpos( $range, '/' ) !== false ) {
506
			if ( self::isIPv6( $range ) ) {
507
				return self::parseRange6( $range );
508
			}
509
			list( $network, $bits ) = self::parseCIDR( $range );
510
			if ( $network === false ) {
511
				$start = $end = false;
512
			} else {
513
				$start = sprintf( '%08X', $network );
514
				$end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 );
515
			}
516
		// Explicit range
517
		} elseif ( strpos( $range, '-' ) !== false ) {
518
			list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
519
			if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) {
520
				return self::parseRange6( $range );
521
			}
522
			if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) {
523
				$start = self::toHex( $start );
524
				$end = self::toHex( $end );
525
				if ( $start > $end ) {
526
					$start = $end = false;
527
				}
528
			} else {
529
				$start = $end = false;
530
			}
531
		} else {
532
			# Single IP
533
			$start = $end = self::toHex( $range );
534
		}
535 View Code Duplication
		if ( $start === false || $end === false ) {
536
			return [ false, false ];
537
		} else {
538
			return [ $start, $end ];
539
		}
540
	}
541
542
	/**
543
	 * Convert a network specification in IPv6 CIDR notation to an
544
	 * integer network and a number of bits
545
	 *
546
	 * @param string $range
547
	 *
548
	 * @return array(string, int)
0 ignored issues
show
The doc-type array(string, could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
549
	 */
550
	private static function parseCIDR6( $range ) {
551
		# Explode into <expanded IP,range>
552
		$parts = explode( '/', IP::sanitizeIP( $range ), 2 );
553
		if ( count( $parts ) != 2 ) {
554
			return [ false, false ];
555
		}
556
		list( $network, $bits ) = $parts;
557
		$network = self::IPv6ToRawHex( $network );
558
		if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) {
559
			if ( $bits == 0 ) {
560
				$network = "0";
561
			} else {
562
				# Native 32 bit functions WONT work here!!!
563
				# Convert to a padded binary number
564
				$network = Wikimedia\base_convert( $network, 16, 2, 128 );
565
				# Truncate the last (128-$bits) bits and replace them with zeros
566
				$network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT );
567
				# Convert back to an integer
568
				$network = Wikimedia\base_convert( $network, 2, 10 );
569
			}
570
		} else {
571
			$network = false;
572
			$bits = false;
573
		}
574
575
		return [ $network, (int)$bits ];
576
	}
577
578
	/**
579
	 * Given a string range in a number of formats, return the
580
	 * start and end of the range in hexadecimal. For IPv6.
581
	 *
582
	 * Formats are:
583
	 *     2001:0db8:85a3::7344/96                       CIDR
584
	 *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
585
	 *     2001:0db8:85a3::7344/96                       Single IP
586
	 *
587
	 * @param string $range
588
	 *
589
	 * @return array(string, string)
0 ignored issues
show
The doc-type array(string, could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
590
	 */
591
	private static function parseRange6( $range ) {
592
		# Expand any IPv6 IP
593
		$range = IP::sanitizeIP( $range );
594
		// CIDR notation...
595
		if ( strpos( $range, '/' ) !== false ) {
596
			list( $network, $bits ) = self::parseCIDR6( $range );
597
			if ( $network === false ) {
598
				$start = $end = false;
599
			} else {
600
				$start = Wikimedia\base_convert( $network, 10, 16, 32, false );
601
				# Turn network to binary (again)
602
				$end = Wikimedia\base_convert( $network, 10, 2, 128 );
603
				# Truncate the last (128-$bits) bits and replace them with ones
604
				$end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT );
605
				# Convert to hex
606
				$end = Wikimedia\base_convert( $end, 2, 16, 32, false );
607
				# see toHex() comment
608
				$start = "v6-$start";
609
				$end = "v6-$end";
610
			}
611
		// Explicit range notation...
612
		} elseif ( strpos( $range, '-' ) !== false ) {
613
			list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
614
			$start = self::toHex( $start );
615
			$end = self::toHex( $end );
616
			if ( $start > $end ) {
617
				$start = $end = false;
618
			}
619
		} else {
620
			# Single IP
621
			$start = $end = self::toHex( $range );
622
		}
623 View Code Duplication
		if ( $start === false || $end === false ) {
624
			return [ false, false ];
625
		} else {
626
			return [ $start, $end ];
627
		}
628
	}
629
630
	/**
631
	 * Determine if a given IPv4/IPv6 address is in a given CIDR network
632
	 *
633
	 * @param string $addr The address to check against the given range.
634
	 * @param string $range The range to check the given address against.
635
	 * @return bool Whether or not the given address is in the given range.
636
	 *
637
	 * @note This can return unexpected results for invalid arguments!
638
	 *       Make sure you pass a valid IP address and IP range.
639
	 */
640
	public static function isInRange( $addr, $range ) {
641
		$hexIP = self::toHex( $addr );
642
		list( $start, $end ) = self::parseRange( $range );
643
644
		return ( strcmp( $hexIP, $start ) >= 0 &&
645
			strcmp( $hexIP, $end ) <= 0 );
646
	}
647
648
	/**
649
	 * Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
650
	 *
651
	 * @since 1.25
652
	 *
653
	 * @param string $ip the IP to check
654
	 * @param array $ranges the IP ranges, each element a range
655
	 *
656
	 * @return bool true if the specified adress belongs to the specified range; otherwise, false.
657
	 */
658
	public static function isInRanges( $ip, $ranges ) {
659
		foreach ( $ranges as $range ) {
660
			if ( self::isInRange( $ip, $range ) ) {
661
				return true;
662
			}
663
		}
664
		return false;
665
	}
666
667
	/**
668
	 * Convert some unusual representations of IPv4 addresses to their
669
	 * canonical dotted quad representation.
670
	 *
671
	 * This currently only checks a few IPV4-to-IPv6 related cases.  More
672
	 * unusual representations may be added later.
673
	 *
674
	 * @param string $addr Something that might be an IP address
675
	 * @return string|null Valid dotted quad IPv4 address or null
676
	 */
677
	public static function canonicalize( $addr ) {
678
		// remove zone info (bug 35738)
679
		$addr = preg_replace( '/\%.*/', '', $addr );
680
681
		if ( self::isValid( $addr ) ) {
682
			return $addr;
683
		}
684
		// Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4
685
		if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) {
686
			$addr = substr( $addr, strrpos( $addr, ':' ) + 1 );
687
			if ( self::isIPv4( $addr ) ) {
688
				return $addr;
689
			}
690
		}
691
		// IPv6 loopback address
692
		$m = [];
693
		if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
694
			return '127.0.0.1';
695
		}
696
		// IPv4-mapped and IPv4-compatible IPv6 addresses
697
		if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) {
698
			return $m[1];
699
		}
700
		if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD .
701
			':' . RE_IPV6_WORD . '$/i', $addr, $m )
702
		) {
703
			return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
704
		}
705
706
		return null; // give up
707
	}
708
709
	/**
710
	 * Gets rid of unneeded numbers in quad-dotted/octet IP strings
711
	 * For example, 127.111.113.151/24 -> 127.111.113.0/24
712
	 * @param string $range IP address to normalize
713
	 * @return string
714
	 */
715
	public static function sanitizeRange( $range ) {
716
		list( /*...*/, $bits ) = self::parseCIDR( $range );
717
		list( $start, /*...*/ ) = self::parseRange( $range );
718
		$start = self::formatHex( $start );
719
		if ( $bits === false ) {
720
			return $start; // wasn't actually a range
721
		}
722
723
		return "$start/$bits";
724
	}
725
726
	/**
727
	 * Returns the subnet of a given IP
728
	 *
729
	 * @param string $ip
730
	 * @return string|false
731
	 */
732
	public static function getSubnet( $ip ) {
733
		$matches = [];
734
		$subnet = false;
735
		if ( IP::isIPv6( $ip ) ) {
736
			$parts = IP::parseRange( "$ip/64" );
737
			$subnet = $parts[0];
738
		} elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
739
			// IPv4
740
			$subnet = $matches[1];
741
		}
742
		return $subnet;
743
	}
744
}
745