Test Failed
Pull Request — 2.1.x (#287)
by Alies
05:45
created

DNSCheckValidation::getWarnings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 1
cts 1
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Egulias\EmailValidator\Validation;
4
5
use Egulias\EmailValidator\EmailLexer;
6
use Egulias\EmailValidator\Exception\DomainAcceptsNoMail;
7
use Egulias\EmailValidator\Exception\InvalidEmail;
8
use Egulias\EmailValidator\Exception\LocalOrReservedDomain;
9
use Egulias\EmailValidator\Exception\NoDNSRecord;
10
use Egulias\EmailValidator\Exception\UnableToGetDNSRecord;
11
use Egulias\EmailValidator\Warning\NoDNSMXRecord;
12
13
class DNSCheckValidation implements EmailValidation
14
{
15
    /**
16
     * @var int
17
     */
18
    protected $dnsRecordTypesToCheck = \DNS_MX;
19
20
    /**
21
     * @var array
22
     */
23
    private $warnings = [];
24
25
    /**
26
     * @var InvalidEmail|null
27
     */
28
    private $error;
29
30 23
    /**
31
     * @var array
32 23
     */
33
    private $mxRecords = [];
34
35 23
36
    public function __construct()
37 23
    {
38
        if (!function_exists('idn_to_ascii')) {
39
            throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__));
40 23
        }
41
42
        $this->dnsRecordTypesToCheck = DNS_MX + DNS_A + DNS_AAAA;
43 23
    }
44 11
45 11
    public function isValid($email, EmailLexer $emailLexer)
46
    {
47
        // use the input to check DNS if we cannot extract something similar to a domain
48 23
        $host = $email;
49
50
        // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email
51
        if (false !== $lastAtPos = strrpos($email, '@')) {
52
            $host = substr($email, $lastAtPos + 1);
53
        }
54 23
55 23
        // Get the domain parts
56 23
        $hostParts = explode('.', $host);
57 23
58
        // Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2),
59
        // mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G)
60 23
        $reservedTopLevelDnsNames = [
61
            // Reserved Top Level DNS Names
62
            'test',
63 23
            'example',
64 23
            'invalid',
65 23
            'localhost',
66 23
67 23
            // mDNS
68 23
            'local',
69 23
70
            // Private DNS Namespaces
71 23
            'intranet',
72 23
            'internal',
73
            'private',
74
            'corp',
75 23
            'home',
76 11
            'lan',
77 11
        ];
78
79
        $isLocalDomain = count($hostParts) <= 1;
80 12
        $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames, true);
81
82
        // Exclude reserved top level DNS names
83 13
        if ($isLocalDomain || $isReservedTopLevel) {
84
            $this->error = new LocalOrReservedDomain();
85 13
            return false;
86
        }
87
88
        return $this->checkDns($host);
89
    }
90
91
    public function getError()
92
    {
93
        return $this->error;
94
    }
95
96
    public function getWarnings()
97
    {
98 12
        return $this->warnings;
99
    }
100 12
101
    /**
102 12
     * @param string $host
103
     *
104 12
     * @return bool
105
     */
106
    protected function checkDns($host)
107
    {
108
        $variant = INTL_IDNA_VARIANT_UTS46;
109
110
        $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.';
111
112
        return $this->validateDnsRecords($host);
113
    }
114
115 12
116
    /**
117
     * Validate the DNS records for given host.
118
     *
119 12
     * @param string $host A set of DNS records in the format returned by dns_get_record.
120
     *
121
     * @return bool True on success.
122
     */
123 12
    private function validateDnsRecords($host)
124 2
    {
125 2
        // A workaround to fix https://bugs.php.net/bug.php?id=73149
126
        set_error_handler(
127
            /**
128
             * @psalm-suppress MissingClosureParamType
129 10
             * @return never-return
0 ignored issues
show
Documentation introduced by
The doc-type never-return could not be parsed: Unknown type name "never-return" at position 0. (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...
130 10
             */
131 1
            static function ($errorLevel, $errorMessage) {
132
                throw new \RuntimeException("Unable to get DNS record for the host: $errorMessage");
133 10
            }
134
        );
135
136 9
        try {
137
        // Get all MX, A and AAAA DNS records for host
138
            $dnsRecords = dns_get_record($host, $this->dnsRecordTypesToCheck);
139
        } catch (\RuntimeException $exception) {
140 9
            $this->error = new UnableToGetDNSRecord();
141
142
            return false;
143
        } finally {
144
            restore_error_handler();
145
        }
146
147
        // No MX, A or AAAA DNS records
148
        if (empty($dnsRecords)) {
149
            $this->error = new NoDNSRecord();
150 10
            return false;
151
        }
152 10
153 10
        // For each DNS record
154
        foreach ($dnsRecords as $dnsRecord) {
155
            if (!$this->validateMXRecord($dnsRecord)) {
156
                return false;
157 10
            }
158 1
        }
159 1
160
        // No MX records (fallback to A or AAAA records)
161
        if (empty($this->mxRecords)) {
162 9
            $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
163
        }
164 9
165
        return true;
166
    }
167
168
    /**
169
     * Validate an MX record
170
     *
171
     * @param array $dnsRecord Given DNS record.
172
     *
173
     * @return bool True if valid.
174
     */
175
    private function validateMxRecord($dnsRecord)
176
    {
177
        if ($dnsRecord['type'] !== 'MX') {
178
            return true;
179
        }
180
181
        // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505)
182
        if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') {
183
            $this->error = new DomainAcceptsNoMail();
184
            return false;
185
        }
186
187
        $this->mxRecords[] = $dnsRecord;
188
189
        return true;
190
    }
191
}
192