1<?php
2/**
3 * Class to validate and to work with IPv6 addresses
4 *
5 * @package Requests\Utilities
6 */
7
8namespace WpOrg\Requests;
9
10use WpOrg\Requests\Exception\InvalidArgument;
11use WpOrg\Requests\Utility\InputValidator;
12
13/**
14 * Class to validate and to work with IPv6 addresses
15 *
16 * This was originally based on the PEAR class of the same name, but has been
17 * entirely rewritten.
18 *
19 * @package Requests\Utilities
20 */
21final class Ipv6 {
22 /**
23 * Uncompresses an IPv6 address
24 *
25 * RFC 4291 allows you to compress consecutive zero pieces in an address to
26 * '::'. This method expects a valid IPv6 address and expands the '::' to
27 * the required number of zero pieces.
28 *
29 * Example: FF01::101 -> FF01:0:0:0:0:0:0:101
30 * ::1 -> 0:0:0:0:0:0:0:1
31 *
32 * @author Alexander Merz <alexander.merz@web.de>
33 * @author elfrink at introweb dot nl
34 * @author Josh Peck <jmp at joshpeck dot org>
35 * @copyright 2003-2005 The PHP Group
36 * @license https://opensource.org/licenses/bsd-license.php
37 *
38 * @param string|Stringable $ip An IPv6 address
39 * @return string The uncompressed IPv6 address
40 *
41 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object.
42 */
43 public static function uncompress($ip) {
44 if (InputValidator::is_string_or_stringable($ip) === false) {
45 throw InvalidArgument::create(1, '$ip', 'string|Stringable', gettype($ip));
46 }
47
48 $ip = (string) $ip;
49
50 if (substr_count($ip, '::') !== 1) {
51 return $ip;
52 }
53
54 list($ip1, $ip2) = explode('::', $ip);
55 $c1 = ($ip1 === '') ? -1 : substr_count($ip1, ':');
56 $c2 = ($ip2 === '') ? -1 : substr_count($ip2, ':');
57
58 if (strpos($ip2, '.') !== false) {
59 $c2++;
60 }
61
62 if ($c1 === -1 && $c2 === -1) {
63 // ::
64 $ip = '0:0:0:0:0:0:0:0';
65 } elseif ($c1 === -1) {
66 // ::xxx
67 $fill = str_repeat('0:', 7 - $c2);
68 $ip = str_replace('::', $fill, $ip);
69 } elseif ($c2 === -1) {
70 // xxx::
71 $fill = str_repeat(':0', 7 - $c1);
72 $ip = str_replace('::', $fill, $ip);
73 } else {
74 // xxx::xxx
75 $fill = ':' . str_repeat('0:', 6 - $c2 - $c1);
76 $ip = str_replace('::', $fill, $ip);
77 }
78
79 return $ip;
80 }
81
82 /**
83 * Compresses an IPv6 address
84 *
85 * RFC 4291 allows you to compress consecutive zero pieces in an address to
86 * '::'. This method expects a valid IPv6 address and compresses consecutive
87 * zero pieces to '::'.
88 *
89 * Example: FF01:0:0:0:0:0:0:101 -> FF01::101
90 * 0:0:0:0:0:0:0:1 -> ::1
91 *
92 * @see \WpOrg\Requests\Ipv6::uncompress()
93 *
94 * @param string $ip An IPv6 address
95 * @return string The compressed IPv6 address
96 */
97 public static function compress($ip) {
98 // Prepare the IP to be compressed.
99 // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method.
100 $ip = self::uncompress($ip);
101 $ip_parts = self::split_v6_v4($ip);
102
103 // Replace all leading zeros
104 $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]);
105
106 // Find bunches of zeros
107 if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) {
108 $max = 0;
109 $pos = null;
110 foreach ($matches[0] as $match) {
111 if (strlen($match[0]) > $max) {
112 $max = strlen($match[0]);
113 $pos = $match[1];
114 }
115 }
116
117 $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max);
118 }
119
120 if ($ip_parts[1] !== '') {
121 return implode(':', $ip_parts);
122 } else {
123 return $ip_parts[0];
124 }
125 }
126
127 /**
128 * Splits an IPv6 address into the IPv6 and IPv4 representation parts
129 *
130 * RFC 4291 allows you to represent the last two parts of an IPv6 address
131 * using the standard IPv4 representation
132 *
133 * Example: 0:0:0:0:0:0:13.1.68.3
134 * 0:0:0:0:0:FFFF:129.144.52.38
135 *
136 * @param string $ip An IPv6 address
137 * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part
138 */
139 private static function split_v6_v4($ip) {
140 if (strpos($ip, '.') !== false) {
141 $pos = strrpos($ip, ':');
142 $ipv6_part = substr($ip, 0, $pos);
143 $ipv4_part = substr($ip, $pos + 1);
144 return [$ipv6_part, $ipv4_part];
145 } else {
146 return [$ip, ''];
147 }
148 }
149
150 /**
151 * Checks an IPv6 address
152 *
153 * Checks if the given IP is a valid IPv6 address
154 *
155 * @param string $ip An IPv6 address
156 * @return bool true if $ip is a valid IPv6 address
157 */
158 public static function check_ipv6($ip) {
159 // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method.
160 $ip = self::uncompress($ip);
161 list($ipv6, $ipv4) = self::split_v6_v4($ip);
162 $ipv6 = explode(':', $ipv6);
163 $ipv4 = explode('.', $ipv4);
164 if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) {
165 foreach ($ipv6 as $ipv6_part) {
166 // The section can't be empty
167 if ($ipv6_part === '') {
168 return false;
169 }
170
171 // Nor can it be over four characters
172 if (strlen($ipv6_part) > 4) {
173 return false;
174 }
175
176 // Remove leading zeros (this is safe because of the above)
177 $ipv6_part = ltrim($ipv6_part, '0');
178 if ($ipv6_part === '') {
179 $ipv6_part = '0';
180 }
181
182 // Check the value is valid
183 $value = hexdec($ipv6_part);
184 if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) {
185 return false;
186 }
187 }
188
189 if (count($ipv4) === 4) {
190 foreach ($ipv4 as $ipv4_part) {
191 $value = (int) $ipv4_part;
192 if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) {
193 return false;
194 }
195 }
196 }
197
198 return true;
199 } else {
200 return false;
201 }
202 }
203}
204