Test Failed
Pull Request — master (#285)
by Alies
02:22
created

DNSCheckValidation::validateDnsRecords()   B

Complexity

Conditions 6
Paths 22

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.6393

Importance

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