Passed
Push — master ( e203b9...deebee )
by Teye
05:19 queued 02:31
created

DynadotApi::__construct()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 48
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.0052

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
dl 0
loc 48
ccs 11
cts 12
cp 0.9167
rs 9.7666
cc 3
nc 1
nop 2
crap 3.0052
1
<?php
2
3
namespace Level23\Dynadot;
4
5
use Psr\Log\LogLevel;
6
use Sabre\Xml\Reader;
7
use GuzzleHttp\Client;
8
use Sabre\Xml\Service;
9
use Psr\Log\LoggerInterface;
10
use Psr\Http\Message\StreamInterface;
11
use Level23\Dynadot\ResultObjects\SetNsResponse;
12
use Level23\Dynadot\ResultObjects\DomainResponse;
13
use Level23\Dynadot\Exception\DynadotApiException;
14
use Level23\Dynadot\ResultObjects\GeneralResponse;
15
use Level23\Dynadot\ResultObjects\DomainInfoResponse;
16
use Level23\Dynadot\ResultObjects\GetContactResponse;
17
use Level23\Dynadot\Exception\ApiHttpCallFailedException;
18
use Level23\Dynadot\ResultObjects\ListDomainInfoResponse;
19
use Level23\Dynadot\Exception\ApiLimitationExceededException;
20
21
/**
22
 * Class DynadotApi
23
 *
24
 * @package Level23\Dynadot
25
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
26
 */
27
class DynadotApi
28
{
29
    const DYNADOT_API_URL = 'https://api.dynadot.com/api3.xml';
30
31
    /**
32
     * This options array is used by Guzzle.
33
     *
34
     * We currently use it to set the Mock Handler in unit testing.
35
     *
36
     * @var array
37
     */
38
    protected $guzzleOptions = [];
39
40
    /**
41
     * Dynadot's API key we should use for HTTP calls.
42
     *
43
     * @var string
44
     */
45
    protected $apiKey;
46
47
    /**
48
     * Logger for writing debug info
49
     *
50
     * @var LoggerInterface|null
51
     */
52
    protected $logger;
53
54
    /**
55
     * Changes boolean values like "no" and "yes" into false and true.
56
     *
57
     * @return bool
58
     * @throws DynadotApiException
59
     * @var \Closure
60
     */
61
    protected $booleanDeserializer;
62
63
    /**
64
     * Return the contact id
65
     *
66
     * @param Reader $reader
67
     *
68
     * @return int
69
     * @var \Closure
70
     */
71
    protected $contactIdDeserializer;
72
73
    /**
74
     * DynadotApi constructor.
75
     *
76
     * @param string                        $apiKey The API key we should use while communicating with the Dynadot API.
77
     * @param \Psr\Log\LoggerInterface|null $logger
78
     *
79
     * @internal param $Logger
80
     */
81 24
    public function __construct(string $apiKey, LoggerInterface $logger = null)
82
    {
83 24
        $this->setApiKey($apiKey);
84 24
        $this->logger = $logger;
85
86
        /**
87
         * Set the default guzzle options
88
         */
89 24
        $this->setGuzzleOptions([
90
            'max'             => 5,
91
            'referer'         => false,
92
            'protocols'       => ['https'],
93
            'connect_timeout' => 30,
94
        ]);
95
96
        /**
97
         * Changes boolean values like "no" and "yes" into false and true.
98
         *
99
         * @param \Sabre\Xml\Reader $reader
100
         *
101
         * @return bool
102
         * @throws \Level23\Dynadot\Exception\DynadotApiException
103
         * @throws \Sabre\Xml\LibXMLException
104
         * @throws \Sabre\Xml\ParseException
105
         */
106 24
        $this->booleanDeserializer = function (Reader $reader) {
107 3
            $value = strtolower($reader->parseInnerTree());
108
109 3
            if ($value != 'yes' && $value != 'no') {
110
                throw new DynadotApiException('Error, received incorrect boolean value ' . var_export($value, true));
111
            }
112
113 3
            return ($value !== 'no');
114
        };
115
116
        /**
117
         * Return the contact id
118
         *
119
         * @param Reader $reader
120
         *
121
         * @return int
122
         * @throws \Sabre\Xml\LibXMLException
123
         * @throws \Sabre\Xml\ParseException
124
         */
125 24
        $this->contactIdDeserializer = function (Reader $reader) {
126 3
            $children = (array)$reader->parseInnerTree();
127
128 3
            return $children[0]['value'];
129
        };
130
    }
131
132
    /**
133
     * @param array $optionsArray
134
     */
135 24
    public function setGuzzleOptions(array $optionsArray)
136
    {
137 24
        $this->guzzleOptions = $optionsArray;
138
    }
139
140
    /**
141
     * Get info about a domain
142
     *
143
     * @param string $domain
144
     *
145
     * @return DomainResponse\Domain
146
     * @throws \Level23\Dynadot\Exception\ApiHttpCallFailedException
147
     * @throws \Level23\Dynadot\Exception\DynadotApiException
148
     * @throws \Sabre\Xml\ParseException
149
     * @throws \GuzzleHttp\Exception\GuzzleException
150
     */
151 7
    public function getDomainInfo(string $domain): DomainResponse\Domain
152
    {
153 7
        $this->log(LogLevel::INFO, 'Retrieve info for domain: ' . $domain);
154
155
        $requestData = [
156
            'domain'  => $domain,
157
            'command' => 'domain_info',
158
        ];
159
160
        // perform the API call
161 7
        $response = $this->performRawApiCall($requestData);
162
163
        // start parsing XML data using Sabre
164 6
        $sabreService = new Service();
165
166
        // set mapping
167 6
        $sabreService->elementMap = [
168 6
            '{}NameServers'          => function (Reader $reader) {
169
170 2
                $nameservers = [];
171 2
                $id          = '';
172
173 2
                $children = (array)$reader->parseInnerTree();
174
175 2
                foreach ($children as $child) {
176 2
                    if ($child['name'] == '{}ServerId') {
177 2
                        $id = $child['value'];
178 2
                    } elseif ($child['name'] == '{}ServerName') {
179 2
                        if (!empty($id) && !empty($child['value'])) {
180 2
                            $nameserver = new DomainResponse\NameServer();
181
182 2
                            $nameserver->ServerId   = $id;
0 ignored issues
show
Documentation Bug introduced by
The property $ServerId was declared of type integer, but $id is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
183 2
                            $nameserver->ServerName = $child['value'];
184
185 2
                            $nameservers[] = $nameserver;
186
                        }
187 2
                        $id = null;
188
                    }
189
                }
190
191 2
                return $nameservers;
192
            },
193 6
            '{}Registrant'           => $this->contactIdDeserializer,
194 6
            '{}Admin'                => $this->contactIdDeserializer,
195 6
            '{}Technical'            => $this->contactIdDeserializer,
196 6
            '{}Billing'              => $this->contactIdDeserializer,
197 6
            '{}isForSale'            => $this->booleanDeserializer,
198 6
            '{}Hold'                 => $this->booleanDeserializer,
199 6
            '{}RegistrantUnverified' => $this->booleanDeserializer,
200 6
            '{}UdrpLocked'           => $this->booleanDeserializer,
201 6
            '{}Disabled'             => $this->booleanDeserializer,
202 6
            '{}Locked'               => $this->booleanDeserializer,
203 6
            '{}WithAds'              => $this->booleanDeserializer,
204
        ];
205
206
        // map certain values to objects
207 6
        $sabreService->mapValueObject('{}DomainInfoResponse', DomainInfoResponse\DomainInfoResponse::class);
208 6
        $sabreService->mapValueObject('{}DomainInfoResponseHeader', DomainInfoResponse\DomainInfoResponseHeader::class);
209 6
        $sabreService->mapValueObject('{}DomainInfoContent', DomainInfoResponse\DomainInfoContent::class);
210 6
        $sabreService->mapValueObject('{}Domain', DomainResponse\Domain::class);
211 6
        $sabreService->mapValueObject('{}NameServerSettings', DomainResponse\NameServerSettings::class);
212 6
        $sabreService->mapValueObject('{}Whois', DomainResponse\Whois::class);
213 6
        $sabreService->mapValueObject('{}Folder', DomainResponse\Folder::class);
214 6
        $sabreService->mapValueObject('{}Response', GeneralResponse\Response::class);
215 6
        $sabreService->mapValueObject('{}ResponseHeader', GeneralResponse\ResponseHeader::class);
216
217 6
        $this->log(LogLevel::DEBUG, 'Start parsing response XML');
218
219
        // parse the data
220 6
        $resultData = $sabreService->parse($response->getContents());
221
222
        // General error, like incorrect api key
223 5
        if ($resultData instanceof GeneralResponse\Response) {
224 1
            $code = $resultData->ResponseHeader->ResponseCode;
225 1
            if ($code != GeneralResponse\ResponseHeader::RESPONSECODE_OK) {
226 1
                throw new DynadotApiException($resultData->ResponseHeader->Error);
227
            }
228
        }
229
230 4
        if (!$resultData instanceof DomainInfoResponse\DomainInfoResponse) {
231 1
            throw new DynadotApiException('We failed to parse the response');
232
        }
233
234 3
        if( $resultData->DomainInfoResponseHeader !== null ) {
235
            /**
236
             * Check if the API call was successful. If not, return the error
237
             */
238 3
            $code = $resultData->DomainInfoResponseHeader->SuccessCode;
239 3
            if ($code != DomainInfoResponse\DomainInfoResponseHeader::SUCCESSCODE_OK) {
240 1
                throw new DynadotApiException($resultData->DomainInfoResponseHeader->Error);
241
            }
242
        }
243
244 2
        $this->log(LogLevel::DEBUG, 'Returning domain info');
245
246
        // Here we know our API call was succesful, return the domain info.
247 2
        return $resultData->DomainInfoContent->Domain;
248
    }
249
250
    /**
251
     * Log a message to our logger, if we have any.
252
     *
253
     * @param string $level
254
     * @param string $message
255
     */
256 23
    protected function log(string $level, string $message): void
257
    {
258 23
        if ($this->logger instanceof LoggerInterface) {
259 1
            $this->logger->log($level, $message);
260
        }
261
    }
262
263
    /**
264
     * Performs the actual API call (internal method)
265
     *
266
     * @param array $requestData
267
     *
268
     * @return \Psr\Http\Message\StreamInterface
269
     * @throws \GuzzleHttp\Exception\GuzzleException
270
     * @throws \Level23\Dynadot\Exception\ApiHttpCallFailedException
271
     */
272 22
    protected function performRawApiCall(array $requestData): StreamInterface
273
    {
274 22
        $this->log(LogLevel::DEBUG, 'Perform raw call: ' . var_export($requestData, true));
275
276
        // transform the request data into a valid query string
277 22
        $requestDataHttp = http_build_query($requestData);
278
279
        // spawn Guzzle
280 22
        $client = new Client($this->guzzleOptions);
281
282 22
        $url = self::DYNADOT_API_URL .
283 22
            '?key=' . urlencode($this->getApiKey()) .
284 22
            ($requestDataHttp ? '&' . $requestDataHttp : '');
285
286 22
        $this->log(LogLevel::DEBUG, 'Start new guzzle request with URL: ' . $url);
287
288
        // start a request with out API key and optionally our request data
289 22
        $response = $client->request('GET', $url);
290
291 22
        $this->log(LogLevel::DEBUG, 'Received response with status code ' . $response->getStatusCode());
292
293
        // if we did not get a HTTP 200 response, our HTTP call failed (which is different from a failed API call)
294 22
        if ($response->getStatusCode() != 200) {
295 1
            $this->log(LogLevel::ALERT, 'Received wrong HTTP status code: ' . $response->getStatusCode());
296
            // not ok
297 1
            throw new ApiHttpCallFailedException(
298 1
                'HTTP API call failed, expected 200 status, got ' . $response->getStatusCode()
299
            );
300
        }
301
302
        // Return the response body (which is a stream coming from Guzzle).
303
        // Sabre XML semi-handles streams (it will just get the contents of the stream using stream_get_contents) so
304
        // this should work! ;)
305 21
        return $response->getBody();
306
    }
307
308
    /**
309
     * @return string
310
     */
311 23
    public function getApiKey(): string
312
    {
313 23
        return $this->apiKey;
314
    }
315
316
    /**
317
     * @param string $apiKey
318
     */
319 24
    public function setApiKey(string $apiKey)
320
    {
321 24
        $this->apiKey = $apiKey;
322
    }
323
324
    /**
325
     * Set nameservers for a domain (max 13). An exception will be thrown in case of an error.
326
     *
327
     * @param string $domain The domain where to set the nameservers for.
328
     * @param array  $nameservers
329
     *
330
     * @throws \GuzzleHttp\Exception\GuzzleException
331
     * @throws \Level23\Dynadot\Exception\ApiHttpCallFailedException
332
     * @throws \Level23\Dynadot\Exception\ApiLimitationExceededException
333
     * @throws \Level23\Dynadot\Exception\DynadotApiException
334
     * @throws \Sabre\Xml\ParseException
335
     */
336 6
    public function setNameserversForDomain(string $domain, array $nameservers)
337
    {
338 6
        $this->log(LogLevel::DEBUG, 'Set ' . sizeof($nameservers) . ' nameservers for domain ' . $domain);
339
        $requestData = [
340
            'command' => 'set_ns',
341
            'domain'  => $domain,
342
        ];
343
344 6
        if (sizeof($nameservers) > 13) {
345
            // index starts at 0, so we should check if the index is greater than 12 (which is 13 nameservers)
346 1
            throw new ApiLimitationExceededException(
347
                'Can not define more than 13 nameservers through the API'
348
            );
349
        }
350
351 5
        $idx = 0;
352
        // check if there are more than 13 nameservers defined
353 5
        foreach ($nameservers as $nameserver) {
354 5
            $requestData['ns' . $idx++] = $nameserver;
355
        }
356
357
        // perform the API call
358 5
        $response = $this->performRawApiCall($requestData);
359
360 5
        $this->log(LogLevel::DEBUG, 'API call executed, parsing response...');
361
362
        // start parsing XML data using Sabre
363 5
        $sabreService = new Service();
364
365
        // map certain values to objects
366 5
        $sabreService->mapValueObject('{}SetNsResponse', SetNsResponse\SetNsResponse::class);
367 5
        $sabreService->mapValueObject('{}SetNsHeader', SetNsResponse\SetNsHeader::class);
368 5
        $sabreService->mapValueObject('{}Response', GeneralResponse\Response::class);
369 5
        $sabreService->mapValueObject('{}ResponseHeader', GeneralResponse\ResponseHeader::class);
370
371
        // parse the data
372 5
        $resultData = $sabreService->parse($response->getContents());
373
374
        // General error, like incorrect api key
375 4
        if ($resultData instanceof GeneralResponse\Response) {
376 1
            $code = $resultData->ResponseHeader->ResponseCode;
377 1
            if ($code != GeneralResponse\ResponseHeader::RESPONSECODE_OK) {
378 1
                throw new DynadotApiException($resultData->ResponseHeader->Error);
379
            }
380
        }
381
382 3
        if (!$resultData instanceof SetNsResponse\SetNsResponse) {
383 1
            throw new DynadotApiException('We failed to parse the response');
384
        }
385
386
        /**
387
         * Check if the API call was successful. If not, return the error
388
         */
389 2
        $code = $resultData->SetNsHeader->SuccessCode;
390 2
        if ($code != SetNsResponse\SetNsHeader::SUCCESSCODE_OK) {
391 1
            throw new DynadotApiException($resultData->SetNsHeader->Error);
392
        }
393
394 1
        $this->log(LogLevel::DEBUG, 'Received correct response. Everything is ok!');
395
    }
396
397
    /**
398
     * List all domains in the account. We will return an array with Domain objects
399
     *
400
     * @return DomainResponse\Domain[]
401
     * @throws \GuzzleHttp\Exception\GuzzleException
402
     * @throws \Level23\Dynadot\Exception\ApiHttpCallFailedException
403
     * @throws \Level23\Dynadot\Exception\DynadotApiException
404
     * @throws \Sabre\Xml\ParseException
405
     */
406 5
    public function getDomainList(): array
407
    {
408 5
        $this->log(LogLevel::DEBUG, 'Start retrieving all domains');
409
        $requestData = [
410
            'command' => 'list_domain',
411
        ];
412
413
        // perform the API call
414 5
        $response = $this->performRawApiCall($requestData);
415
416 5
        $this->log(LogLevel::DEBUG, 'Start parsing response XML');
417
418
        // start parsing XML data using Sabre
419 5
        $sabreService = new Service();
420
421
        // set mapping
422 5
        $sabreService->elementMap = [
423 5
            '{}NameServers'          => function (Reader $reader) {
424
425
                $nameservers = [];
426
                $id          = '';
427
428
                $children = (array)$reader->parseInnerTree();
429
430
                foreach ($children as $child) {
431
                    if ($child['name'] == '{}ServerId') {
432
                        $id = $child['value'];
433
                    } elseif ($child['name'] == '{}ServerName') {
434
                        if (!empty($id) && !empty($child['value'])) {
435
                            $nameserver             = new DomainResponse\NameServer();
436
                            $nameserver->ServerId   = $id;
0 ignored issues
show
Documentation Bug introduced by
The property $ServerId was declared of type integer, but $id is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
437
                            $nameserver->ServerName = $child['value'];
438
439
                            $nameservers[] = $nameserver;
440
                        }
441
                        $id = null;
442
                    }
443
                }
444
445
                return $nameservers;
446
            },
447 5
            '{}DomainInfoList'       => function (Reader $reader) {
448 1
                $domains = [];
449
450 1
                $tree = (array)$reader->parseInnerTree();
451
452 1
                foreach ($tree as $item) {
453 1
                    foreach ($item['value'] as $domain ) {
454 1
                        $domains[] = $domain['value'];
455
                    }
456
                }
457
458 1
                return $domains;
459
            },
460 5
            '{}Registrant'           => $this->contactIdDeserializer,
461 5
            '{}Admin'                => $this->contactIdDeserializer,
462 5
            '{}Technical'            => $this->contactIdDeserializer,
463 5
            '{}Billing'              => $this->contactIdDeserializer,
464 5
            '{}isForSale'            => $this->booleanDeserializer,
465 5
            '{}Hold'                 => $this->booleanDeserializer,
466 5
            '{}RegistrantUnverified' => $this->booleanDeserializer,
467 5
            '{}UdrpLocked'           => $this->booleanDeserializer,
468 5
            '{}Disabled'             => $this->booleanDeserializer,
469 5
            '{}Locked'               => $this->booleanDeserializer,
470 5
            '{}WithAds'              => $this->booleanDeserializer,
471
        ];
472
473
        // map certain values to objects
474 5
        $sabreService->mapValueObject('{}ListDomainInfoResponse', ListDomainInfoResponse\ListDomainInfoResponse::class);
475 5
        $sabreService->mapValueObject('{}ListDomainInfoHeader', ListDomainInfoResponse\ListDomainInfoHeader::class);
476 5
        $sabreService->mapValueObject('{}ListDomainInfoContent', ListDomainInfoResponse\ListDomainInfoContent::class);
477 5
        $sabreService->mapValueObject('{}Domain', DomainResponse\Domain::class);
478 5
        $sabreService->mapValueObject('{}NameServerSettings', DomainResponse\NameServerSettings::class);
479 5
        $sabreService->mapValueObject('{}Whois', DomainResponse\Whois::class);
480 5
        $sabreService->mapValueObject('{}Folder', DomainResponse\Folder::class);
481 5
        $sabreService->mapValueObject('{}Response', GeneralResponse\Response::class);
482 5
        $sabreService->mapValueObject('{}ResponseHeader', GeneralResponse\ResponseHeader::class);
483
484
        // parse the data
485 5
        $resultData = $sabreService->parse($response->getContents());
486
487
        // General error, like incorrect api key
488 4
        if ($resultData instanceof GeneralResponse\Response) {
489 1
            $code = $resultData->ResponseHeader->ResponseCode;
490 1
            if ($code != GeneralResponse\ResponseHeader::RESPONSECODE_OK) {
491 1
                throw new DynadotApiException($resultData->ResponseHeader->Error);
492
            }
493
        }
494
495 3
        if (!$resultData instanceof ListDomainInfoResponse\ListDomainInfoResponse) {
496 1
            throw new DynadotApiException('We failed to parse the response');
497
        }
498
499
        /**
500
         * Check if the API call was successful. If not, return the error
501
         */
502 2
        $code = $resultData->ListDomainInfoHeader->ResponseCode;
503 2
        if ($code != ListDomainInfoResponse\ListDomainInfoHeader::RESPONSECODE_OK) {
504 1
            throw new DynadotApiException($resultData->ListDomainInfoHeader->Error);
505
        }
506
507 1
        return $resultData->ListDomainInfoContent->DomainInfoList;
508
    }
509
510
    /**
511
     * Get contact information for a specific contact ID
512
     *
513
     * @param int $contactId The contact ID we should request
514
     *
515
     * @return GetContactResponse\Contact
516
     * @throws \GuzzleHttp\Exception\GuzzleException
517
     * @throws \Level23\Dynadot\Exception\ApiHttpCallFailedException
518
     * @throws \Level23\Dynadot\Exception\DynadotApiException
519
     * @throws \Sabre\Xml\ParseException
520
     */
521 5
    public function getContactInfo(int $contactId): GetContactResponse\Contact
522
    {
523 5
        $this->log(LogLevel::DEBUG, 'Fetch contact details for id ' . $contactId);
524
525
        $requestData = [
526
            'command'    => 'get_contact',
527
            'contact_id' => $contactId,
528
        ];
529
530
        // perform the API call
531 5
        $response = $this->performRawApiCall($requestData);
532
533 5
        $this->log(LogLevel::DEBUG, 'Start parsing result');
534
535
        // start parsing XML data using Sabre
536 5
        $sabreService = new Service();
537
538
        // map certain values to objects
539 5
        $sabreService->mapValueObject('{}GetContactResponse', GetContactResponse\GetContactResponse::class);
540 5
        $sabreService->mapValueObject('{}GetContactHeader', GetContactResponse\GetContactHeader::class);
541 5
        $sabreService->mapValueObject('{}GetContactContent', GetContactResponse\GetContactContent::class);
542 5
        $sabreService->mapValueObject('{}Contact', GetContactResponse\Contact::class);
543 5
        $sabreService->mapValueObject('{}Response', GeneralResponse\Response::class);
544 5
        $sabreService->mapValueObject('{}ResponseHeader', GeneralResponse\ResponseHeader::class);
545
546
        // parse the data
547 5
        $resultData = $sabreService->parse($response->getContents());
548
549
        // General error, like incorrect api key
550 4
        if ($resultData instanceof GeneralResponse\Response) {
551 1
            $code = $resultData->ResponseHeader->ResponseCode;
552 1
            if ($code != GeneralResponse\ResponseHeader::RESPONSECODE_OK) {
553 1
                throw new DynadotApiException($resultData->ResponseHeader->Error);
554
            }
555
        }
556
557 3
        if (!$resultData instanceof GetContactResponse\GetContactResponse) {
558 1
            throw new DynadotApiException('We failed to parse the response');
559
        }
560
561
        /**
562
         * Check if the API call was successful. If not, return the error
563
         */
564 2
        $code = $resultData->GetContactHeader->ResponseCode;
565 2
        if ($code != GetContactResponse\GetContactHeader::RESPONSECODE_OK) {
566 1
            throw new DynadotApiException($resultData->GetContactHeader->Error);
567
        }
568
569 1
        return $resultData->GetContactContent->Contact;
570
    }
571
}
572