Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.55% covered (warning)
79.55%
35 / 44
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dns_Providers
83.33% covered (warning)
83.33%
35 / 42
80.00% covered (warning)
80.00%
4 / 5
29.13
0.00% covered (danger)
0.00%
0 / 1
 match_nameserver
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 match_nameservers
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
9
 match_domain_connect
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 is_known
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 matches
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2/**
3 * DNS providers known to Send, matched by nameserver.
4 *
5 * @package automattic/jetpack-send
6 */
7
8namespace Automattic\Jetpack\Send;
9
10if ( ! defined( 'ABSPATH' ) ) {
11    exit( 0 );
12}
13
14/**
15 * PHP copy of the nameserver patterns in `_inc/data/providers.json`, plus the hosts whose
16 * addresses can't be used for sending.
17 */
18class Dns_Providers {
19
20    /**
21     * Providers by slug: `array( 'name' => string, 'ns' => string[], 'match' => 'suffix'|'substring' )`.
22     * Must list the same slugs and patterns as `_inc/data/providers.json`, in the same (rank) order.
23     *
24     * @var array<string, array{name: string, ns: string[], match: string}>
25     */
26    const PROVIDERS = array(
27        'cloudflare'   => array(
28            'name'  => 'Cloudflare',
29            'ns'    => array( '.ns.cloudflare.com' ),
30            'match' => 'suffix',
31        ),
32        'bluehost'     => array(
33            'name'  => 'Bluehost',
34            'ns'    => array( 'ns1.bluehost.com', 'ns2.bluehost.com', '.bluehost.com', '.mybluehost.me', '.bluehost.in', '.hostmonster.com', '.justhost.com' ),
35            'match' => 'suffix',
36        ),
37        'hostinger'    => array(
38            'name'  => 'Hostinger',
39            'ns'    => array( '.dns-parking.com' ),
40            'match' => 'suffix',
41        ),
42        'godaddy'      => array(
43            'name'  => 'GoDaddy',
44            'ns'    => array( '.domaincontrol.com' ),
45            'match' => 'suffix',
46        ),
47        'hostgator'    => array(
48            'name'  => 'HostGator',
49            'ns'    => array( '.hostgator.com', '.hostgator.com.br', '.hostgator.mx', '.websitewelcome.com' ),
50            'match' => 'suffix',
51        ),
52        'siteground'   => array(
53            'name'  => 'SiteGround',
54            'ns'    => array( '.siteground.net' ),
55            'match' => 'suffix',
56        ),
57        'ionos'        => array(
58            'name'  => 'IONOS',
59            'ns'    => array( '.ui-dns.com', '.ui-dns.de', '.ui-dns.org', '.ui-dns.biz' ),
60            'match' => 'suffix',
61        ),
62        'namecheap'    => array(
63            'name'  => 'Namecheap',
64            'ns'    => array( '.registrar-servers.com', '.namecheaphosting.com' ),
65            'match' => 'suffix',
66        ),
67        'ovhcloud'     => array(
68            'name'  => 'OVHcloud',
69            'ns'    => array( '.ovh.net', '.anycast.me', '.ovh.ca' ),
70            'match' => 'suffix',
71        ),
72        'dreamhost'    => array(
73            'name'  => 'DreamHost',
74            'ns'    => array( '.dreamhost.com' ),
75            'match' => 'suffix',
76        ),
77        'route53'      => array(
78            'name'  => 'Amazon Route 53',
79            'ns'    => array( '.awsdns-' ),
80            'match' => 'substring',
81        ),
82        'squarespace'  => array(
83            'name'  => 'Squarespace (incl. former Google Domains)',
84            'ns'    => array( '.squarespacedns.com', '.googledomains.com' ),
85            'match' => 'suffix',
86        ),
87        'wordpresscom' => array(
88            'name'  => 'WordPress.com',
89            'ns'    => array( '.wordpress.com' ),
90            'match' => 'suffix',
91        ),
92        'other'        => array(
93            'name'  => 'Other or not sure',
94            'ns'    => array(),
95            'match' => 'suffix',
96        ),
97    );
98
99    /**
100     * Domain Connect hosts seen for each provider, mirroring `domainConnectHost` in `_inc/data/providers.json`.
101     *
102     * @var array<string, string>
103     */
104    const DOMAIN_CONNECT_HOSTS = array(
105        'cloudflare'   => 'api.cloudflare.com/client/v4/dns/domainconnect',
106        'godaddy'      => 'domainconnect.api.godaddy.com',
107        'ionos'        => 'api.domainconnect.ionos.com',
108        'squarespace'  => 'domains.squarespace.com',
109        'wordpresscom' => 'public-api.wordpress.com/rest/v1.3/domain-connect',
110    );
111
112    /**
113     * Nameserver suffix that another DNS service shares with Squarespace, so it needs Domain Connect to confirm.
114     *
115     * @var string
116     */
117    const SHARED_SQUARESPACE_SUFFIX = '.googledomains.com';
118
119    /**
120     * Host suffixes that are temporary or staging addresses, never a domain to send from.
121     *
122     * @var string[]
123     */
124    const NON_EDITABLE_SUFFIXES = array(
125        'wpcomstaging.com',
126        'mystagingwebsite.com',
127        'mybluehost.me',
128        'hostingersite.com',
129        'wpengine.com',
130        'wpenginepowered.com',
131        'pressable.com',
132        'kinsta.cloud',
133        'flywheelsites.com',
134        'flywheelstaging.com',
135        'pantheonsite.io',
136        'cloudwaysapps.com',
137        'myftpupload.com',
138        'instawp.xyz',
139        'tastewp.com',
140        'playground.wordpress.net',
141        'ddev.site',
142        'lndo.site',
143        'ngrok.io',
144        'ngrok-free.app',
145        'trycloudflare.com',
146        'local',
147    );
148
149    /**
150     * Reserved top-level names (RFC 2606 / RFC 6761) that can't have real DNS.
151     *
152     * @var string[]
153     */
154    const RESERVED_TLDS = array( 'example', 'invalid', 'localhost', 'test' );
155
156    /**
157     * The provider slug for one nameserver.
158     *
159     * @param string $nameserver Lowercased hostname, no trailing dot.
160     * @return string|null Slug, or null when no pattern matches.
161     */
162    public static function match_nameserver( $nameserver ) {
163        $nameserver = strtolower( rtrim( trim( (string) $nameserver ), '.' ) );
164        if ( '' === $nameserver ) {
165            return null;
166        }
167        foreach ( self::PROVIDERS as $slug => $provider ) {
168            foreach ( $provider['ns'] as $pattern ) {
169                if ( self::matches( $nameserver, $pattern, $provider['match'] ) ) {
170                    return $slug;
171                }
172            }
173        }
174        return null;
175    }
176
177    /**
178     * The provider for a set of nameservers: majority wins, a mixed set lowers confidence.
179     *
180     * @param string[] $nameservers Lowercased hostnames.
181     * @return array{provider: string|null, confidence: string} Confidence is `high`, `low` or `none`.
182     */
183    public static function match_nameservers( array $nameservers ) {
184        $nameservers = array_values( array_unique( array_filter( array_map( 'strval', $nameservers ) ) ) );
185        $counts      = array();
186        foreach ( $nameservers as $nameserver ) {
187            $slug           = self::match_nameserver( $nameserver );
188            $key            = null === $slug ? '' : $slug;
189            $counts[ $key ] = ( $counts[ $key ] ?? 0 ) + 1;
190        }
191
192        // Most matches wins; ties go to the higher-ranked provider (PROVIDERS order).
193        $best = null;
194        foreach ( array_keys( self::PROVIDERS ) as $slug ) {
195            if ( isset( $counts[ $slug ] ) && ( null === $best || $counts[ $slug ] > $counts[ $best ] ) ) {
196                $best = $slug;
197            }
198        }
199        if ( null === $best ) {
200            return array(
201                'provider'   => null,
202                'confidence' => 'none',
203            );
204        }
205        return array(
206            'provider'   => $best,
207            'confidence' => count( $nameservers ) === $counts[ $best ] ? 'high' : 'low',
208        );
209    }
210
211    /**
212     * The provider whose known Domain Connect host matches a discovered one.
213     *
214     * @param string $host Host from a `_domainconnect` record.
215     * @return string|null Slug, or null when none matches.
216     */
217    public static function match_domain_connect( $host ) {
218        $host = strtolower( trim( (string) $host ) );
219        if ( '' === $host ) {
220            return null;
221        }
222        foreach ( self::DOMAIN_CONNECT_HOSTS as $slug => $known ) {
223            if ( false !== strpos( $host, $known ) ) {
224                return $slug;
225            }
226        }
227        return null;
228    }
229
230    /**
231     * Whether a slug is a known provider (including `other` and `unsure`).
232     *
233     * @param string $slug Slug.
234     * @return bool
235     */
236    public static function is_known( $slug ) {
237        return is_string( $slug ) && ( 'unsure' === $slug || isset( self::PROVIDERS[ $slug ] ) );
238    }
239
240    /**
241     * Whether a nameserver matches a pattern: a suffix on a label boundary, or a plain substring.
242     *
243     * @param string $nameserver Lowercased hostname.
244     * @param string $pattern    Pattern from PROVIDERS.
245     * @param string $mode       `suffix` or `substring`.
246     * @return bool
247     */
248    private static function matches( $nameserver, $pattern, $mode ) {
249        if ( 'substring' === $mode ) {
250            return false !== strpos( $nameserver, $pattern );
251        }
252        if ( '.' !== $pattern[0] ) {
253            if ( $nameserver === $pattern ) {
254                return true;
255            }
256            $pattern = '.' . $pattern;
257        }
258        return strlen( $nameserver ) > strlen( $pattern ) && substr( $nameserver, -strlen( $pattern ) ) === $pattern;
259    }
260}