xref: /dokuwiki/vendor/simplepie/simplepie/src/Net/IPv6.php (revision 8e88a29b81301f78509349ab1152bb09c229123e)
1<?php
2
3// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
4// SPDX-License-Identifier: BSD-3-Clause
5
6declare(strict_types=1);
7
8namespace SimplePie\Net;
9
10/**
11 * Class to validate and to work with IPv6 addresses.
12 *
13 * @copyright 2003-2005 The PHP Group
14 * @license http://www.opensource.org/licenses/bsd-license.php
15 * @link http://pear.php.net/package/Net_IPv6
16 * @author Alexander Merz <alexander.merz@web.de>
17 * @author elfrink at introweb dot nl
18 * @author Josh Peck <jmp at joshpeck dot org>
19 * @author Sam Sneddon <geoffers@gmail.com>
20 */
21class IPv6
22{
23    /**
24     * Uncompresses an IPv6 address
25     *
26     * RFC 4291 allows you to compress consecutive zero pieces in an address to
27     * '::'. This method expects a valid IPv6 address and expands the '::' to
28     * the required number of zero pieces.
29     *
30     * Example:  FF01::101   ->  FF01:0:0:0:0:0:0:101
31     *           ::1         ->  0:0:0:0:0:0:0:1
32     *
33     * @author Alexander Merz <alexander.merz@web.de>
34     * @author elfrink at introweb dot nl
35     * @author Josh Peck <jmp at joshpeck dot org>
36     * @copyright 2003-2005 The PHP Group
37     * @license http://www.opensource.org/licenses/bsd-license.php
38     * @param string $ip An IPv6 address
39     * @return string The uncompressed IPv6 address
40     */
41    public static function uncompress(string $ip)
42    {
43        $c1 = -1;
44        $c2 = -1;
45        if (substr_count($ip, '::') === 1) {
46            [$ip1, $ip2] = explode('::', $ip);
47            if ($ip1 === '') {
48                $c1 = -1;
49            } else {
50                $c1 = substr_count($ip1, ':');
51            }
52            if ($ip2 === '') {
53                $c2 = -1;
54            } else {
55                $c2 = substr_count($ip2, ':');
56            }
57            if (strpos($ip2, '.') !== false) {
58                $c2++;
59            }
60            // ::
61            if ($c1 === -1 && $c2 === -1) {
62                $ip = '0:0:0:0:0:0:0:0';
63            }
64            // ::xxx
65            elseif ($c1 === -1) {
66                $fill = str_repeat('0:', 7 - $c2);
67                $ip = str_replace('::', $fill, $ip);
68            }
69            // xxx::
70            elseif ($c2 === -1) {
71                $fill = str_repeat(':0', 7 - $c1);
72                $ip = str_replace('::', $fill, $ip);
73            }
74            // xxx::xxx
75            else {
76                $fill = ':' . str_repeat('0:', 6 - $c2 - $c1);
77                $ip = str_replace('::', $fill, $ip);
78            }
79        }
80        return $ip;
81    }
82
83    /**
84     * Compresses an IPv6 address
85     *
86     * RFC 4291 allows you to compress consecutive zero pieces in an address to
87     * '::'. This method expects a valid IPv6 address and compresses consecutive
88     * zero pieces to '::'.
89     *
90     * Example:  FF01:0:0:0:0:0:0:101   ->  FF01::101
91     *           0:0:0:0:0:0:0:1        ->  ::1
92     *
93     * @see uncompress()
94     * @param string $ip An IPv6 address
95     * @return string The compressed IPv6 address
96     */
97    public static function compress(string $ip)
98    {
99        // Prepare the IP to be compressed
100        $ip = self::uncompress($ip);
101        $ip_parts = self::split_v6_v4($ip);
102
103        // Replace all leading zeros
104        $ip_parts[0] = (string) 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            assert($pos !== null, 'For PHPStan: Since the regex matched, there is at least one match. And because the pattern is non-empty, the loop will always end with $pos ≥ 1.');
118            $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max);
119        }
120
121        if ($ip_parts[1] !== '') {
122            return implode(':', $ip_parts);
123        }
124
125        return $ip_parts[0];
126    }
127
128    /**
129     * Splits an IPv6 address into the IPv6 and IPv4 representation parts
130     *
131     * RFC 4291 allows you to represent the last two parts of an IPv6 address
132     * using the standard IPv4 representation
133     *
134     * Example:  0:0:0:0:0:0:13.1.68.3
135     *           0:0:0:0:0:FFFF:129.144.52.38
136     *
137     * @param string $ip An IPv6 address
138     * @return array{string, string} [0] contains the IPv6 represented part, and [1] the IPv4 represented part
139     */
140    private static function split_v6_v4(string $ip): array
141    {
142        if (strpos($ip, '.') !== false) {
143            $pos = strrpos($ip, ':');
144            assert($pos !== false, 'For PHPStan: IPv6 address must contain colon, since split_v6_v4 is only ever called after uncompress.');
145            $ipv6_part = substr($ip, 0, $pos);
146            $ipv4_part = substr($ip, $pos + 1);
147            return [$ipv6_part, $ipv4_part];
148        }
149
150        return [$ip, ''];
151    }
152
153    /**
154     * Checks an IPv6 address
155     *
156     * Checks if the given IP is a valid IPv6 address
157     *
158     * @param string $ip An IPv6 address
159     * @return bool true if $ip is a valid IPv6 address
160     */
161    public static function check_ipv6(string $ip)
162    {
163        $ip = self::uncompress($ip);
164        [$ipv6, $ipv4] = self::split_v6_v4($ip);
165        $ipv6 = explode(':', $ipv6);
166        $ipv4 = explode('.', $ipv4);
167        if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) {
168            foreach ($ipv6 as $ipv6_part) {
169                // The section can't be empty
170                if ($ipv6_part === '') {
171                    return false;
172                }
173
174                // Nor can it be over four characters
175                if (strlen($ipv6_part) > 4) {
176                    return false;
177                }
178
179                // Remove leading zeros (this is safe because of the above)
180                $ipv6_part = ltrim($ipv6_part, '0');
181                if ($ipv6_part === '') {
182                    $ipv6_part = '0';
183                }
184
185                // Check the value is valid
186                $value = hexdec($ipv6_part);
187                if ($value < 0 || $value > 0xFFFF) {
188                    return false;
189                }
190                assert(is_int($value), 'For PHPStan: $value is only float when $ipv6_part > PHP_INT_MAX');
191                if (dechex($value) !== strtolower($ipv6_part)) {
192                    return false;
193                }
194            }
195            if (count($ipv4) === 4) {
196                foreach ($ipv4 as $ipv4_part) {
197                    $value = (int) $ipv4_part;
198                    if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) {
199                        return false;
200                    }
201                }
202            }
203            return true;
204        }
205
206        return false;
207    }
208
209    /**
210     * Checks if the given IP is a valid IPv6 address
211     *
212     * @codeCoverageIgnore
213     * @deprecated Use {@see IPv6::check_ipv6()} instead
214     * @see check_ipv6
215     * @param string $ip An IPv6 address
216     * @return bool true if $ip is a valid IPv6 address
217     */
218    public static function checkIPv6(string $ip)
219    {
220        return self::check_ipv6($ip);
221    }
222}
223
224class_alias('SimplePie\Net\IPv6', 'SimplePie_Net_IPv6');
225