Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
186 / 186
100.00% covered (success)
100.00%
33 / 33
CRAP
100.00% covered (success)
100.00%
1 / 1
Client
100.00% covered (success)
100.00%
186 / 186
100.00% covered (success)
100.00%
33 / 33
79
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 setCustomLogger
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultLogger
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 enableDebugMode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 disableDebugMode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPOSTData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getSession
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getURL
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUserAgent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getUserAgent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setProxy
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getProxy
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setReferer
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getReferer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveSession
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 reuseSession
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setURL
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setOTP
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setSession
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setRemoteIPAddress
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setCredentials
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setRoleCredentials
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 IDNConvert
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 flattenCommand
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 autoIDNConvert
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
12
 request
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
5
 requestNextResponsePage
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 requestAllResponsePages
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 setUserView
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 useHighPerformanceConnectionSetup
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 useOTESystem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 useLIVESystem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3#declare(strict_types=1);
4
5/**
6 * CNIC\HEXONET
7 * Copyright © CentralNic Group PLC
8 */
9
10namespace CNIC\HEXONET;
11
12use CNIC\HEXONET\ResponseTemplateManager as RTM;
13use CNIC\HEXONET\Logger as L;
14
15/**
16 * HEXONET API Client
17 *
18 * @package CNIC\HEXONET
19 */
20
21class Client
22{
23    /**
24     * registrar api settings
25     * @var array
26     */
27    public $settings;
28    /**
29     * API connection url
30     * @var string
31     */
32    protected $socketURL;
33    /**
34     * Object covering API connection data
35     * @var SocketConfig
36     */
37    protected $socketConfig;
38    /**
39     * activity flag for debug mode
40     * @var boolean
41     */
42    protected $debugMode;
43    /**
44     * user agent
45     * @var string
46     */
47    protected $ua;
48    /**
49     * additional curl options to use
50     * @var array
51     */
52    protected $curlopts = [];
53    /**
54     * logger function name for debug mode
55     * @var \CNIC\LoggerInterface
56     */
57    protected $logger;
58    /**
59     * is connected to OT&E
60     * @var bool
61     */
62    public $isOTE = false;
63
64    /**
65     * Constructor
66     *
67     * @param string $path Path to the configuration file
68     */
69    public function __construct($path = "")
70    {
71        $contents = file_get_contents($path) ?: "[]";
72        /** @var array $settings */
73        $settings = json_decode($contents, true);
74        $this->settings = $settings;
75        $this->socketURL = "";
76        $this->debugMode = false;
77        $this->ua = "";
78        $this->socketConfig = new SocketConfig($this->settings["parameters"]);
79        $this->useLIVESystem();
80        $this->setDefaultLogger();
81    }
82
83    /**
84     * set custom logger to use instead of default one
85     * create your own class implementing \CNIC\LoggerInterface
86     * @param \CNIC\LoggerInterface $customLogger
87     * @return $this
88     */
89    public function setCustomLogger($customLogger)
90    {
91        $this->logger = $customLogger;
92        return $this;
93    }
94
95    /**
96     * set default logger to use
97     * @return $this
98     */
99    public function setDefaultLogger()
100    {
101        $this->logger = new L();
102        return $this;
103    }
104
105    /**
106     * Enable Debug Output to STDOUT
107     * @return $this
108     */
109    public function enableDebugMode()
110    {
111        $this->debugMode = true;
112        return $this;
113    }
114
115    /**
116     * Disable Debug Output
117     * @return $this
118     */
119    public function disableDebugMode()
120    {
121        $this->debugMode = false;
122        return $this;
123    }
124
125    /**
126     * Serialize given command for POST request including connection configuration data
127     * @param string|array $cmd API command to encode
128     * @param bool $secured secure password (when used for output)
129     * @return string encoded POST data string
130     */
131    public function getPOSTData($cmd, $secured = false)
132    {
133        if (is_string($cmd)) {
134            $command = [];
135            parse_str($cmd, $command);
136        } else {
137            $command = $cmd;
138        }
139        return $this->socketConfig->getPOSTData($command, $secured);
140    }
141
142    /**
143     * Get the API Session ID that is currently set
144     * @return string|null API Session ID currently in use
145     */
146    public function getSession()
147    {
148        $sessid = $this->socketConfig->getSession();
149        return ($sessid === "" ? null : $sessid);
150    }
151
152    /**
153     * Get the API connection url that is currently set
154     * @return string API connection url currently in use
155     */
156    public function getURL()
157    {
158        return $this->socketURL;
159    }
160
161    /**
162     * Set a custom user agent (for platforms that use this SDK)
163     * @param string $str user agent label
164     * @param string $rv user agent revision
165     * @param array $modules further modules to add to user agent string, format: ["<module1>/<version>", "<module2>/<version>", ... ]
166     * @return $this
167     */
168    public function setUserAgent($str, $rv, $modules = [])
169    {
170        $mods = empty($modules) ? "" : " " . implode(" ", $modules);
171        $this->ua = (
172            $str . " (" . PHP_OS . "; " . php_uname("m") . "; rv:" . $rv . ")" . $mods . " php-sdk/" . $this->getVersion() . " php/" . implode(".", [PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION])
173        );
174        return $this;
175    }
176
177    /**
178     * Get the user agent string
179     * @return string user agent string
180     */
181    public function getUserAgent()
182    {
183        if (!strlen($this->ua)) {
184            $this->ua = "PHP-SDK (" . PHP_OS . "; " . php_uname("m") . "; rv:" . $this->getVersion() . ") php/" . implode(".", [PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION]);
185        }
186        return $this->ua;
187    }
188
189    /**
190     * Set proxy to use for API communication
191     * @param string $proxy proxy to use (optional, for reset)
192     * @return $this
193     */
194    public function setProxy($proxy = "")
195    {
196        if (empty($proxy)) {
197            unset($this->curlopts[CURLOPT_PROXY]);
198        } else {
199            $this->curlopts[CURLOPT_PROXY] = $proxy;
200        }
201        return $this;
202    }
203
204    /**
205     * Get proxy configuration for API communication
206     * @return string|null
207     */
208    public function getProxy()
209    {
210        if (isset($this->curlopts[CURLOPT_PROXY])) {
211            return $this->curlopts[CURLOPT_PROXY];
212        }
213        return null;
214    }
215
216    /**
217     * Set Referer to use for API communication
218     * @param string $referer Referer (optional, for reset)
219     * @return $this
220     */
221    public function setReferer($referer = "")
222    {
223        if (empty($referer)) {
224            unset($this->curlopts[CURLOPT_REFERER]);
225        } else {
226            $this->curlopts[CURLOPT_REFERER] = $referer;
227        }
228        return $this;
229    }
230
231    /**
232     * Get Referer configuration for API communication
233     * @return string|null
234     */
235    public function getReferer()
236    {
237        if (isset($this->curlopts[CURLOPT_REFERER])) {
238            return $this->curlopts[CURLOPT_REFERER];
239        }
240        return null;
241    }
242
243    /**
244     * Get the current module version
245     * @return string module version
246     */
247    public function getVersion()
248    {
249        return "8.0.2";
250    }
251
252    /**
253     * Apply session data (session id and system entity) to given php session object
254     * @param array $session php session instance ($_SESSION)
255     * @return $this
256     */
257    public function saveSession(&$session)
258    {
259        $session["socketcfg"] = [
260            "entity" => $this->socketConfig->getSystemEntity(),
261            "session" => $this->socketConfig->getSession()
262        ];
263        return $this;
264    }
265
266    /**
267     * Use existing configuration out of php session object
268     * to rebuild and reuse connection settings
269     * @param array $session php session object ($_SESSION)
270     * @return $this
271     */
272    public function reuseSession(&$session)
273    {
274        $this->socketConfig->setSystemEntity($session["socketcfg"]["entity"]);
275        $this->setSession($session["socketcfg"]["session"]);
276        return $this;
277    }
278
279    /**
280     * Set another connection url to be used for API communication
281     * @param string $value API connection url to set
282     * @return $this
283     */
284    public function setURL($value)
285    {
286        $this->socketURL = $value;
287        return $this;
288    }
289
290    /**
291     * Set one time password to be used for API communication
292     * @param string $value one time password (optional, for reset)
293     * @throws \Exception in case this feature is not supported
294     * @return $this
295     */
296    public function setOTP($value = "")
297    {
298        if (!empty($value) && !isset($this->settings["parameters"]["otp"])) {
299            throw new \Exception("Feature `OTP` not supported");
300        }
301        $this->socketConfig->setOTP($value);
302        return $this;
303    }
304
305    /**
306     * Set an API session id to be used for API communication
307     * @param string $value API session id (optional, for reset)
308     * @return $this
309     */
310    public function setSession($value = "")
311    {
312        $this->socketConfig->setSession($value);
313        return $this;
314    }
315
316    /**
317     * Set an Remote IP Address to be used for API communication
318     * To be used in case you have an active ip filter setting.
319     * @param string $value Remote IP Address (optional, for reset)
320     * @throws \Exception in case this feature is unsupported
321     * @return $this
322     */
323    public function setRemoteIPAddress($value = "")
324    {
325        if (!empty($value) && !isset($this->settings["parameters"]["ipfilter"])) {
326            throw new \Exception("Feature `IP Filter` not supported");
327        }
328        $this->socketConfig->setRemoteAddress($value);
329        return $this;
330    }
331
332    /**
333     * Set Credentials to be used for API communication
334     * @param string $uid account name (optional, for reset)
335     * @param string $pw account password (optional, for reset)
336     * @return $this
337     */
338    public function setCredentials($uid = "", $pw = "")
339    {
340        $this->socketConfig->setLogin($uid);
341        $this->socketConfig->setPassword($pw);
342        return $this;
343    }
344
345    /**
346     * Set Credentials to be used for API communication
347     * @param string $uid account name (optional, for reset)
348     * @param string $role role user id (optional, for reset)
349     * @param string $pw role user password (optional, for reset)
350     * @return $this
351     */
352    public function setRoleCredentials($uid = "", $role = "", $pw = "")
353    {
354        $login = $uid;
355        if (!empty($role)) {
356            $login .= $this->settings["roleSeparator"] . $role;
357        }
358        return $this->setCredentials($login, $pw);
359    }
360
361    /**
362     * Convert domain names to idn + punycode if necessary
363     * @param array $domains list of domain names (or tlds)
364     * @return array
365     */
366    public function IDNConvert($domains)
367    {
368        $results = [];
369        foreach ($domains as $idx => $d) {
370            $results[$idx] = [
371                "PUNYCODE" => $d,
372                "IDN" => $d
373            ];
374        }
375        if ($this->settings["needsIDNConvert"]) {
376            $r = $this->request([
377                "COMMAND" => "ConvertIDN",
378                "DOMAIN" => $domains
379            ]);
380            if ($r->isSuccess()) {
381                $results = [];
382                $col1 = $r->getColumn("ACE");
383                $col2 = $r->getColumn("IDN");
384                if (!is_null($col1) && !is_null($col2)) {
385                    $d1 = $col1->getData();
386                    $d2 = $col2->getData();
387                    foreach ($domains as $idx => $d) {
388                        if (isset($d1[$idx], $d2[$idx])) {
389                            $results[$idx]["PUNYCODE"] = $d1[$idx];
390                            $results[$idx]["IDN"] = $d2[$idx];
391                        }
392                    }
393                }
394            }
395        }
396        return $results;
397    }
398
399    /**
400     * Flatten API command's nested arrays for easier handling
401     * @param array $cmd API Command
402     * @return array
403     */
404    protected function flattenCommand($cmd)
405    {
406        $newcmd = [];
407        foreach ($cmd as $key => $val) {
408            if (isset($val)) {
409                $val = preg_replace("/\r|\n/", "", $val);
410                $newKey = \strtoupper($key);
411                if (is_array($val)) {
412                    foreach ($cmd[$key] as $idx => $v) {
413                        $newcmd[$newKey . $idx] = $v;
414                    }
415                } else {
416                    $newcmd[$newKey] = $val;
417                }
418            }
419        }
420        return $newcmd;
421    }
422
423    /**
424     * Auto convert API command parameters to punycode, if necessary.
425     * @param array $cmd API command
426     * @return array
427     */
428    protected function autoIDNConvert($cmd)
429    {
430        // only convert if configured for the registrar
431        // don't convert for convertidn command to avoid endless loop
432        if (
433            !$this->settings["needsIDNConvert"]
434            || preg_match("/^CONVERTIDN$/i", $cmd["COMMAND"])
435        ) {
436            return $cmd;
437        }
438        $cmdkeys = array_keys($cmd);
439        $asciipattern = "/^[a-z0-9\.\-]+$/i";
440        $keypattern = "/^(DOMAIN|NAMESERVER|DNSZONE|OBJECTID)([0-9]*)$/i";
441        $objclasspattern = "/^(DOMAIN|DELETEDDOMAIN|DOMAINAPPLICATION|NAMESERVER|DNSZONE)$/i";
442        $keys = preg_grep($keypattern, $cmdkeys);
443        if (empty($keys)) {
444            return $cmd;
445        }
446        $toconvert = [];
447        $idxs = [];
448        foreach ($keys as $key) {
449            if (
450                isset($cmd[$key])
451                && !(bool)preg_match($asciipattern, $cmd[$key])
452                && !empty($cmd[$key])
453                && (
454                    ($key !== "OBJECTID")
455                    || preg_match($objclasspattern, $cmd["OBJECTCLASS"])
456                )
457            ) {
458                $toconvert[] = $cmd[$key];
459                $idxs[] = $key;
460            }
461        }
462        if (!empty($toconvert)) {
463            $results = $this->IDNConvert($toconvert);
464            foreach ($results as $idx => $row) {
465                $cmd[$idxs[$idx]] = $row["PUNYCODE"];
466            }
467        }
468        return $cmd;
469    }
470
471    /**
472     * Perform API request using the given command
473     * @param array $cmd API command to request
474     * @return Response Response
475     */
476    public function request($cmd)
477    {
478        // flatten nested api command bulk parameters
479        $mycmd = $this->flattenCommand($cmd);
480        // auto convert umlaut names to punycode
481        $mycmd = $this->autoIDNConvert($mycmd);
482        // request command to API
483        $cfg = [
484            "CONNECTION_URL" => $this->socketURL
485        ];
486        $data = $this->getPOSTData($mycmd);
487        $curl = curl_init($cfg["CONNECTION_URL"]);
488        // PHP 7.3 return false vs. 7.4 throws an Exception
489        // when setting the URL to "\0"
490        // @codeCoverageIgnoreStart
491        if ($curl === false) {
492            $r = new Response("nocurl", $mycmd, $cfg);
493            if ($this->debugMode) {
494                $secured = $this->getPOSTData($mycmd, true);
495                $this->logger->log($secured, $r, "CURL for PHP missing.");
496            }
497            return $r;
498        }
499        // @codeCoverageIgnoreEnd
500        curl_setopt_array($curl, [
501            // CURLOPT_VERBOSE         => $this->debugMode,
502            CURLOPT_CONNECTTIMEOUT  => 5000,
503            CURLOPT_TIMEOUT         => $this->settings["socketTimeout"],
504            CURLOPT_POST            => 1,
505            CURLOPT_POSTFIELDS      => $data,
506            CURLOPT_HEADER          => 0,
507            CURLOPT_RETURNTRANSFER  => 1,
508            CURLOPT_USERAGENT       => $this->getUserAgent(),
509            CURLOPT_HTTPHEADER      => [
510                "Expect:",
511                "Content-Type: application/x-www-form-urlencoded",//UTF-8 implied
512                "Content-Length: " . strlen($data)
513            ]
514        ] + $this->curlopts);
515
516        // curl_exec with CURLOPT_USERAGENT returns string|false and not string|bool
517        // which is by default tested for by phpStan
518        /** @var string|false $r */
519        $r = curl_exec($curl);
520        $error = null;
521        if ($r === false) {
522            $r = "httperror";
523            $error = curl_error($curl);
524        }
525        $response = new Response($r, $mycmd, $cfg);
526
527        curl_close($curl);
528        if ($this->debugMode) {
529            $secured = $this->getPOSTData($mycmd, true);
530            $this->logger->log($secured, $response, $error);
531        }
532        return $response;
533    }
534
535    /**
536     * Request the next page of list entries for the current list query
537     * Useful for tables
538     * @param Response $rr API Response of current page
539     * @throws \Exception in case Command Parameter LAST is in use while using this method
540     * @return Response|null Response or null in case there are no further list entries
541     */
542    public function requestNextResponsePage($rr)
543    {
544        $mycmd = $rr->getCommand();
545        if (array_key_exists("LAST", $mycmd)) {
546            throw new \Exception("Parameter LAST in use. Please remove it to avoid issues in requestNextPage.");
547        }
548        $first = 0;
549        if (array_key_exists("FIRST", $mycmd)) {
550            $first = $mycmd["FIRST"];
551        }
552        $total = $rr->getRecordsTotalCount();
553        $limit = $rr->getRecordsLimitation();
554        $first += $limit;
555        if ($first < $total) {
556            $mycmd["FIRST"] = $first;
557            $mycmd["LIMIT"] = $limit;
558            return $this->request($mycmd);
559        }
560
561        return null;
562    }
563
564    /**
565     * Request all pages/entries for the given query command
566     * @param array $cmd API list command to use
567     * @return Response[] Responses
568     */
569    public function requestAllResponsePages($cmd)
570    {
571        $responses = [];
572        $rr = $this->request(array_merge([], $cmd, ["FIRST" => 0]));
573        $tmp = $rr;
574        $idx = 0;
575        do {
576            $responses[$idx++] = $tmp;
577            $tmp = $this->requestNextResponsePage($tmp);
578        } while ($tmp !== null);
579        return $responses;
580    }
581
582    /**
583     * Set a data view to a given subuser
584     * @param string $uid subuser account name
585     * @return $this
586     */
587    public function setUserView($uid = "")
588    {
589        $this->socketConfig->setUser($uid);
590        return $this;
591    }
592
593    /**
594     * Activate High Performance Setup
595     * @return $this
596     */
597    public function useHighPerformanceConnectionSetup()
598    {
599        $oldurl = $this->getURL();
600        $hostname = parse_url($oldurl, PHP_URL_HOST);
601        if (!empty($hostname)) {
602            $url = str_replace($hostname, "127.0.0.1", $oldurl);
603            $url = str_replace("https://", "http://", $url);
604            $this->setURL($url);
605        }
606        return $this;
607    }
608
609    /**
610     * Set OT&E System for API communication
611     * @return $this
612     */
613    public function useOTESystem()
614    {
615        if (isset($this->settings["env"]["ote"]["entity"])) {
616            $this->socketConfig->setSystemEntity($this->settings["env"]["ote"]["entity"]);
617        }
618        $this->isOTE = true;
619        return $this->setURL($this->settings["env"]["ote"]["url"]);
620    }
621
622    /**
623     * Set LIVE System for API communication (this is the default setting)
624     * @return $this
625     */
626    public function useLIVESystem()
627    {
628        if (isset($this->settings["env"]["ote"]["entity"])) {
629            $this->socketConfig->setSystemEntity($this->settings["env"]["live"]["entity"]);
630        }
631        $this->isOTE = false;
632        return $this->setURL($this->settings["env"]["live"]["url"]);
633    }
634}