Completed
Pull Request — master (#132)
by Paweł
62:51
created

Common::checkIDNSupport()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
/**
4
 * League.Uri (https://uri.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Uri;
15
16
use League\Uri\Exceptions\IdnSupportMissing;
17
use League\Uri\Exceptions\SyntaxError;
18
19
final class Common
20
{
21
    /**
22
     * Range of invalid characters in URI string.
23
     *
24
     * @see https://tools.ietf.org/html/rfc3986#section-2.2
25
     *
26
     * @var string
27
     */
28
    public const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
29
30
    /**
31
     * Invalid characters in host regular expression pattern.
32
     *
33
     * @see https://tools.ietf.org/html/rfc3986#section-3.2.2
34
     */
35
    public const REGEXP_INVALID_HOST_CHARS = '/
36
        [:\/?#\[\]@ ]  # gen-delims characters as well as the space character
37
    /ix';
38
39
    /**
40
     * RFC3986 IPvFuture regular expression pattern.
41
     *
42
     * @see https://tools.ietf.org/html/rfc3986#section-3.2.2
43
     *
44
     * @var string
45
     */
46
    public const REGEXP_HOST_IPFUTURE = '/^
47
        v(?<version>[A-F0-9])+\.
48
        (?:
49
            (?<unreserved>[a-z0-9_~\-\.])|
50
            (?<sub_delims>[!$&\'()*+,;=:])  # also include the : character
51
        )+
52
    $/ix';
53
54
    /**
55
     * RFC3986 host identified by a registered name regular expression pattern.
56
     *
57
     * @see https://tools.ietf.org/html/rfc3986#section-3.2.2
58
     *
59
     * @var string
60
     */
61
    public const REGEXP_HOST_REGISTERED_NAME = '/(?(DEFINE)
62
        (?<unreserved>[a-z0-9_~\-])   # . is missing as it is used to separate labels
63
        (?<sub_delims>[!$&\'()*+,;=])
64
        (?<encoded>%[A-F0-9]{2})
65
        (?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
66
    )
67
    ^(?:(?&reg_name)\.)*(?&reg_name)\.?$/ix';
68
69
    /**
70
     * Only the address block fe80::/10 can have a Zone ID attach to
71
     * let's detect the link local significant 10 bits.
72
     */
73
    public const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80";
74
75
    /**
76
     * IDNA errors.
77
     *
78
     * @var array
79
     */
80
    private const IDNA_ERRORS = [
81
        IDNA_ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
82
        IDNA_ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
83
        IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
84
        IDNA_ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
85
        IDNA_ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
86
        IDNA_ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
87
        IDNA_ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
88
        IDNA_ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
89
        IDNA_ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
90
        IDNA_ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
91
        IDNA_ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
92
        IDNA_ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
93
        IDNA_ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
94
    ];
95
96
    /**
97
     * Retrieves and format IDNA conversion error message.
98
     *
99
     * @see http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html
100
     */
101
    public static function getIDNAErrors(int $error_byte): string
102
    {
103
        $res = [];
104
105
        foreach (self::IDNA_ERRORS as $error => $reason) {
106
            if ($error === ($error_byte & $error)) {
107
                $res[] = $reason;
108
            }
109
        }
110
111
        return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.';
112
    }
113
114
    /**
115
     * Validate and format a registered name.
116
     *
117
     * The host is converted to its ascii representation if needed
118
     *
119
     * @throws IdnSupportMissing if the submitted host required missing or misconfigured IDN support
120
     * @throws SyntaxError       if the submitted host is not a valid registered name
121
     */
122
    public static function filterRegisteredName(string $host, bool $format = true): string
123
    {
124
        $formatted_host = rawurldecode($host);
125
126
        if (1 === preg_match(Common::REGEXP_HOST_REGISTERED_NAME, $formatted_host)) {
127
            if (false === strpos($formatted_host, 'xn--')) {
128
                return $format ? strtolower($formatted_host) : $host;
129
            }
130
131
            self::checkIDN($host);
132
133
            return $format ? strtolower($host) : $host;
134
        }
135
136
        if (1 === preg_match(Common::REGEXP_INVALID_HOST_CHARS, $formatted_host)) {
137
            throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host));
138
        }
139
140
        $idn = self::checkIDN($formatted_host, false);
141
142
        if (false !== strpos($idn, '%')) {
143
            throw new SyntaxError(sprintf('Host `%s` is invalid : the host is not a valid registered name', $host));
144
        }
145
146
        return $format ? $idn : $host;
147
    }
148
149
    /**
150
     * Filter IDN domain to UTF8 or ASCII.
151
     *
152
     * @throws SyntaxError
153
     * @throws IdnSupportMissing
154
     */
155
    private static function checkIDN(string $host, bool $uft8 = true): string
156
    {
157
        self::checkIDNSupport($host);
158
159
        if ($uft8) {
160
            $host = strtolower($host);
161
            $convert = idn_to_utf8(
162
                $host,
163
                IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_UNICODE,
164
                INTL_IDNA_VARIANT_UTS46,
165
                $idna_info
166
            );
167
        } else {
168
            $convert = idn_to_ascii(
169
                $host,
170
                IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_ASCII,
171
                INTL_IDNA_VARIANT_UTS46,
172
                $idna_info
173
            );
174
        }
175
176
        if (0 !== $idna_info['errors']) {
177
            throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, Common::getIDNAErrors($idna_info['errors'])));
178
        }
179
180
        // @codeCoverageIgnoreStart
181
        // added because it is not possible in travis to disabled the ext/intl extension
182
        // see travis issue https://github.com/travis-ci/travis-ci/issues/4701
183
        if (false === $convert) {
184
            throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
185
        }
186
        // @codeCoverageIgnoreEnd
187
188
        return $idna_info['result'] ?? '';
189
    }
190
191
    /**
192
     * Check IDN support.
193
     *
194
     * @codeCoverageIgnore
195
     * added because it is not possible in travis to disabled the ext/intl extension
196
     * see travis issue https://github.com/travis-ci/travis-ci/issues/4701
197
     *
198
     * @throws IdnSupportMissing
199
     */
200
    private static function checkIDNSupport(string $host): void
201
    {
202
        static $idn_support = null;
203
        $idn_support = $idn_support ?? function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46');
204
205
        if (!$idn_support) {
206
            throw new IdnSupportMissing(sprintf('The host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
207
        }
208
    }
209
}
210