From 4054fa46082f48394ca75a714ddd3283d0a998d7 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 20:36:11 +0100 Subject: [PATCH] Add Domains --- Layout/default/Domain/Index.php | 240 ++++++++++++++++++ application/Domain/Domain.php | 9 + application/Domain/DomainController.php | 196 ++++++++++++++ application/Domain/DomainModel.php | 133 ++++++++++ application/DomainContact/DomainContact.php | 8 + .../DomainContact/DomainContactModel.php | 121 +++++++++ db/migrations/20240312203000_add_domain.php | 64 +++++ lib/inwx/inwx.php | 155 +++++++++++ lib/plesk/plesk.php | 36 +++ 9 files changed, 962 insertions(+) create mode 100644 Layout/default/Domain/Index.php create mode 100644 application/Domain/Domain.php create mode 100644 application/Domain/DomainController.php create mode 100644 application/Domain/DomainModel.php create mode 100644 application/DomainContact/DomainContact.php create mode 100644 application/DomainContact/DomainContactModel.php create mode 100644 db/migrations/20240312203000_add_domain.php create mode 100644 lib/inwx/inwx.php create mode 100644 lib/plesk/plesk.php diff --git a/Layout/default/Domain/Index.php b/Layout/default/Domain/Index.php new file mode 100644 index 000000000..e080c4b5a --- /dev/null +++ b/Layout/default/Domain/Index.php @@ -0,0 +1,240 @@ + self::getUrl("Domain"), + "DASHBOARD_URL" => self::getUrl("Dashboard"), + "MFAPPNAME" => MFAPPNAME_SLUG, + "PAGE_TITLE" => "Domains", + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], + ["text" => "Domain Management", "href" => self::getUrl("Domain")], + ["text" => "Domains"] + ], + "DOMAIN_API_URL" => self::getUrl("Domain/api"), +]; + +$additionalJS = ["plugins/vue/vue.js", + "plugins/axios/axios.min.js", + "plugins/vue/tt-components/tt-page-title.js", + "plugins/vue/tt-components/tt-table.js", +]; +$additionalCSS = [ + 'plugins/vue/tt-components/css/tt-table.css', +]; + +include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/application/Domain/Domain.php b/application/Domain/Domain.php new file mode 100644 index 000000000..bff2e8496 --- /dev/null +++ b/application/Domain/Domain.php @@ -0,0 +1,9 @@ +loadMe(); + $this->layout()->set("me", $me); + $this->me = $me; + + $this->inwx = new Inwx($this->INWX_USER, $this->INWX_PASS); + $this->plesk = new Plesk($this->PLESK_USER, $this->PLESK_AUTH); + } + + protected function indexAction(): void { + $this->layout()->setTemplate("Domain/Index"); + } + + protected function apiAction() { + $do = $this->request->do; + + if ($do !== "getConfig" && !$this->me->is("employee")) { + $this->redirect("dashboard"); + } + switch ($do) { + case "importAllDomains": + $return = $this->importAllDomains(); + break; + case "getDomains": + $return = $this->getAllDomains(); + break; + case "getDomainContacts": + $return = $this->getDomainContacts(); + break; + case "getDnsRecords": + $return = $this->getDnsRecords(); + break; + case "checkDomain": + $return = $this->checkDomain(); + break; + default: + $return = false; + break; + } + + if (!$return) { + $return = [ + "status" => "error", + "message" => "Invalid request." + ]; + } + + die(json_encode($return)); + } + + protected function importDomain(): void { + // use plesk api to get all domains + + + } + + protected function importAllDomains(): array { + + try { + $inwxContact = $this->inwx->contactList(); + $pleskDomains = $this->plesk->getAllDomains(); + $inwxDomains = $this->inwx->domainList(); + + $domains = []; + $pleskDomainsArray = []; + + foreach ($pleskDomains as $pleskDomain) { + $pleskDomainsArray[$pleskDomain['name']] = $pleskDomain; + } + + foreach ($inwxDomains as $inwxDomain) { + if (isset($pleskDomainsArray[$inwxDomain['domain']])) { + $inwxDomain['plesk'] = [ + "id" => $pleskDomainsArray[$inwxDomain['domain']]['id'], + "hosting_type" => $pleskDomainsArray[$inwxDomain['domain']]['hosting_type'], + "created" => strtotime($pleskDomainsArray[$inwxDomain['domain']]['created']) + ]; + } + + $domains[] = $inwxDomain; + } + + $domainsImport = DomainModel::importDomains($domains); + $contactsImport = DomainContactModel::importDomainContacts($inwxContact); + + return [ + "status" => "success", + "importMessages" => [ + $domainsImport['message'], + $contactsImport['message'] + ], + ]; + } catch (Exception $e) { + $this->log->error("Error while importing domains: " . $e->getMessage()); + return [ + "status" => "error", + "message" => "Error while importing domains: " . $e->getMessage() + ]; + } + + } + + private function getAllDomains(): array { + + $json = json_decode(file_get_contents('php://input'), true); + + $filters = $json['filters'] ?? []; + $page = $json['pagination']['page'] ?? 1; + $perPage = $json['pagination']['per_page'] ?? 10; + + $domains = DomainModel::getAllDomains($filters, $perPage, $perPage * $page - $perPage); + $totalRows = DomainModel::countDomains($filters); + + return [ + "rows" => $domains, + "pagination" => [ + "page" => $page, + "total_pages" => ceil($totalRows / $perPage), + "per_page" => $perPage, + "total_rows" => intval($totalRows) + ] + ]; + + } + + private function getDnsRecords() { + if (!isset($this->request->domain)) { + return ["status" => "error", "message" => "No domain specified."]; + } + + $domain = $this->request->domain; + return array_merge( + dns_get_record($domain, DNS_TXT), + dns_get_record($domain, DNS_A), + dns_get_record($domain, DNS_CNAME), + dns_get_record($domain, DNS_MX), + dns_get_record($domain, DNS_NS), + dns_get_record($domain, DNS_SOA), + dns_get_record($domain, DNS_SRV), + dns_get_record($domain, DNS_AAAA), + + ); + } + + private function getDomainContacts(): array { + $domainContacts = []; + $dbDomainContacts = DomainContactModel::getAllDomainContacts(); + + foreach ($dbDomainContacts as $dbDomainContact) { + $domainContacts[$dbDomainContact['inwxRoId']] = $dbDomainContact; + } + + return $domainContacts; + } + + private function checkDomain(): array { + $domain = $this->request->domain; + + if(empty($domain)) { + return ["status" => "error", "message" => "No domain or tld specified."]; + } + + try { + $domainCheck = $this->inwx->domainCheck($domain); + + if($domainCheck['domain'][0]['status'] === "free") { + $domainPrice = $this->inwx->domainGetDomainPrice($domain, "reg"); + } else { + $domainPrice = $this->inwx->domainGetDomainPrice($domain, "transfer"); + } + + $domainCheck['domain'][0]['price'] = $domainPrice; + + return $domainCheck['domain'][0]; + } catch (Exception $e) { + $this->log->error("Error while checking domain: " . $e->getMessage()); + return ["status" => "error", "message" => "Error while checking domain: " . $e->getMessage()]; + } + } + + +} \ No newline at end of file diff --git a/application/Domain/DomainModel.php b/application/Domain/DomainModel.php new file mode 100644 index 000000000..60d13d016 --- /dev/null +++ b/application/Domain/DomainModel.php @@ -0,0 +1,133 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + public static function importDomains($domains): array { + $db = FronkDB::singleton(); + + $db->query("TRUNCATE TABLE `Domain`"); + + $sql = /** @lang text */ + "INSERT INTO `Domain` (`inwxRoId`, `domain`, `period`, `crDate`, `exDate`, `reDate`, `upDate`, `transferLock`, `status`, `authCode`, `registrant`, `admin`, `tech`, `billing`, `ns`, `pleskId`, `pleskHostingType`, `pleskCreated`) VALUES "; + $values = []; + foreach ($domains as $domain) { + $valueStr ="(" . + $domain['roId'] . ", '" . + $domain['domain'] . "', '" . + $domain['period'] . "', " . + $domain['crDate']['timestamp'] . ", " . + $domain['exDate']['timestamp'] . ", " . + $domain['reDate']['timestamp'] . ", " . + $domain['upDate']['timestamp'] . ", " . + ($domain['transferLock'] ? 1 : 0) . ", '" . + $domain['status'] . "', '" . + $domain['authCode'] . "', " . + $domain['registrant'] . ", " . + $domain['admin'] . ", " . + $domain['tech'] . ", " . + $domain['billing'] . ", '" . + implode(", ", $domain['ns']) . "', "; + + // Check if 'pleskId' is set + if (isset($domain['plesk']) && is_array($domain['plesk'])) { + $valueStr .= $domain['plesk']['id'] . ", "; + $valueStr .= "'" . $domain['plesk']['hosting_type'] . "', "; + $valueStr .= $domain['plesk']['created']; + } else { + $valueStr .= "NULL, NULL, NULL"; + } + + $values[] = $valueStr . ")"; + } + $sql .= implode(", ", $values); + $db->query($sql); + return [ + "message" => "Imported " . count($domains) . " domains." + ]; + } + + /** + * Generate SQL Filter condition (space separated) for a given column. + * + * @param string|null $filterValue The filter value to match against. + * @param string $columnName The name of the column in the database table. + * @return string The SQL condition generated based on the filter value and column name. + */ + public static function generateFilterCondition(?string $filterValue, string $columnName): string { + $sql = ""; + if (!empty($filterValue)) { + $filterItems = explode(" ", $filterValue); + foreach ($filterItems as $item) { + $sql .= " AND `$columnName` LIKE '%" . $item . "%'"; + } + } + + return $sql; + } + + public static function getSqlFilter($filters): string { + $sql = isset($filters['crDate']) ? self::generateFilterCondition($filters['domain'], "domain") : ""; + $sql .= isset($filters['crDate']) ? " AND `crDate` = " . $filters['crDate'] : ""; + $sql .= isset($filters['exDate']) ? " AND `exDate` = " . $filters['exDate'] : ""; + $sql .= isset($filters['reDate']) ? " AND `reDate` = " . $filters['reDate'] : ""; + $sql .= isset($filters['upDate']) ? " AND `upDate` = " . $filters['upDate'] : ""; + $sql .= isset($filters['status']) ? " AND `status` = '" . $filters['status'] . "'" : ""; + $sql .= isset($filters['transferLock']) && $filters['transferLock'] == 1 ? " AND `transferLock` = true" : ""; + $sql .= isset($filters['authCode']) ? self::generateFilterCondition($filters['authCode'], "authCode") : ""; + $sql .= isset($filters['registrant']) && $filters['registrant'] !== 'all' ? " AND `registrant` = " . $filters['registrant'] : ""; + $sql .= isset($filters['admin']) && $filters['admin'] !== 'all' ? " AND `admin` = " . $filters['admin'] : ""; + $sql .= isset($filters['tech']) && $filters['tech'] !== 'all' ? " AND `tech` = " . $filters['tech'] : ""; + $sql .= isset($filters['billing']) && $filters['billing'] !== 'all' ? " AND `billing` = " . $filters['billing'] : ""; + $sql .= isset($filters['ns']) ? self::generateFilterCondition($filters['ns'], "ns") : ""; + return $sql; + + } + + public static function getAllDomains($filters, $limit = null, $offset = 0): array { + $db = FronkDB::singleton(); + $sql = "SELECT * FROM `Domain` WHERE 1 " . self::getSqlFilter($filters); + $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; + + $result = $db->query($sql); + $rows = []; + while ($row = $result->fetch_assoc()) { + $rows[] = new DomainModel($row); + } + + return $rows; + } + + public static function countDomains($filters) { + $db = FronkDB::singleton(); + $sql = "SELECT COUNT(*) as `total_rows` FROM `Domain` WHERE 1 " . self::getSqlFilter($filters); + $result = $db->query($sql); + return $result->fetch_assoc()['total_rows']; + } +} \ No newline at end of file diff --git a/application/DomainContact/DomainContact.php b/application/DomainContact/DomainContact.php new file mode 100644 index 000000000..19f738f64 --- /dev/null +++ b/application/DomainContact/DomainContact.php @@ -0,0 +1,8 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + public static function importDomainContacts($domainContacts): array { + $db = FronkDB::singleton(); + + $db->query("TRUNCATE TABLE `DomainContact`"); + + $sql = /** @lang text */ + "INSERT INTO `DomainContact` (`inwxRoId`, `type`, `name`, `street`, `city`, `pc`, `cc`, `voice`, `email`, `protection`, `verificationStatus`, `usedCount`) VALUES "; + $values = []; + + foreach ($domainContacts as $domainContact) { + $valueStr = "(" . + $domainContact['roId'] . ", '" . + $domainContact['type'] . "', '" . + $domainContact['name'] . "', '" . + $domainContact['street'] . "', '" . + $domainContact['city'] . "', '" . + $domainContact['pc'] . "', '" . + $domainContact['cc'] . "', '" . + $domainContact['voice'] . "', '" . + $domainContact['email'] . "', " . + ($domainContact['protection'] ? 1 : 0) . ", '" . + $domainContact['verificationStatus'] . "', "; + + $valueStr .= $domainContact['usedCount'] ?? "NULL"; + $valueStr .= ")"; + + $values[] = $valueStr; + } + + $sql .= implode(", ", $values); + $db->query($sql); + return [ + "message" => "Imported " . count($domainContacts) . " domain contacts." + ]; + } + + /** + * Generate SQL Filter condition (space separated) for a given column. + * + * @param string|null $filterValue The filter value to match against. + * @param string $columnName The name of the column in the database table. + * @return string The SQL condition generated based on the filter value and column name. + */ + public static function generateFilterCondition(?string $filterValue, string $columnName): string { + $sql = ""; + if (!empty($filterValue)) { + $filterItems = explode(" ", $filterValue); + foreach ($filterItems as $item) { + $sql .= " AND `$columnName` LIKE '%" . $item . "%'"; + } + } + + return $sql; + } + + public static function getSqlFilter($filters): string { + $sql = isset($filters['roId']) ? " AND `inwxRoId` = " . $filters['roId'] : ""; + $sql .= isset($filters['type']) ? " AND `type` = '" . $filters['type'] . "'" : ""; + $sql .= isset($filters['name']) ? self::generateFilterCondition($filters['name'], "name") : ""; + $sql .= isset($filters['street']) ? self::generateFilterCondition($filters['street'], "street") : ""; + $sql .= isset($filters['city']) ? self::generateFilterCondition($filters['city'], "city") : ""; + $sql .= isset($filters['pc']) ? " AND `pc` = " . $filters['pc'] : ""; + $sql .= isset($filters['cc']) ? " AND `cc` = " . $filters['cc'] : ""; + $sql .= isset($filters['voice']) ? " AND `voice` = " . $filters['voice'] : ""; + $sql .= isset($filters['email']) ? self::generateFilterCondition($filters['email'], "email") : ""; + $sql .= isset($filters['protection']) ? " AND `protection` = " . $filters['protection'] : ""; + $sql .= isset($filters['usedCount']) ? " AND `usedCount` = " . $filters['usedCount'] : ""; + $sql .= isset($filters['verificationStatus']) ? " AND `verificationStatus` = '" . $filters['verificationStatus'] . "'" : ""; + return $sql; + + } + + public static function getAllDomainContacts($filters = null, $limit = null, $offset = 0, $raw_array = true): array { + $db = FronkDB::singleton(); + $sql = "SELECT * FROM `DomainContact` WHERE 1 "; + $sql .= $filters === null ? "" : self::getSqlFilter($filters); + $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; + + $result = $db->query($sql); + $rows = []; + while ($row = $result->fetch_assoc()) { + $rows[] = $raw_array ? $row : new DomainContactModel($row); + } + + return $rows; + } + + public static function countDomainContacts($filters) { + $db = FronkDB::singleton(); + $sql = "SELECT COUNT(*) as `total_rows` FROM `DomainContact` WHERE 1 " . self::getSqlFilter($filters); + $result = $db->query($sql); + return $result->fetch_assoc()['total_rows']; + } +} \ No newline at end of file diff --git a/db/migrations/20240312203000_add_domain.php b/db/migrations/20240312203000_add_domain.php new file mode 100644 index 000000000..f3a13c65a --- /dev/null +++ b/db/migrations/20240312203000_add_domain.php @@ -0,0 +1,64 @@ +getEnvironment() == "thetool") { + //Domain Table + $domainTable = $this->table("Domain", ["signed" => true]); + $domainTable->addColumn("inwxRoId", "integer", ["null" => true]); + $domainTable->addColumn("domain", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("period", "string", ["null" => true, "limit" => 50]); + $domainTable->addColumn("crDate", "integer", ["null" => true]); + $domainTable->addColumn("exDate", "integer", ["null" => true]); + $domainTable->addColumn("reDate", "integer", ["null" => true]); + $domainTable->addColumn("upDate", "integer", ["null" => true]); + $domainTable->addColumn("transferLock", "integer", ["null" => true, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $domainTable->addColumn("status", "string", ["null" => true, "limit" => 50]); + $domainTable->addColumn("authCode", "string", ["null" => true, "limit" => 50]); + $domainTable->addColumn("registrant", "integer", ["null" => true]); + $domainTable->addColumn("admin", "integer", ["null" => true]); + $domainTable->addColumn("tech", "integer", ["null" => true]); + $domainTable->addColumn("billing", "integer", ["null" => true]); + $domainTable->addColumn("ns", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("pleskId", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("pleskHostingType", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("pleskCreated", "integer", ["null" => true]); + $domainTable->save(); + + //DomainContact Table + + $domainContactTable = $this->table("DomainContact", ["signed" => true]); + $domainContactTable->addColumn("inwxRoId", "integer", ["null" => true]); + $domainContactTable->addColumn("type", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("name", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("street", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("city", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("pc", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("cc", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("voice", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("email", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("protection", "integer", ["null" => true, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $domainContactTable->addColumn("usedCount", "integer", ["null" => true]); + $domainContactTable->addColumn("verificationStatus", "string", ["null" => true, "limit" => 255]); + $domainContactTable->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void { + if ($this->getEnvironment() == "thetool") { + $this->table("Domain")->drop()->save(); + $this->table("DomainContact")->drop()->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/lib/inwx/inwx.php b/lib/inwx/inwx.php new file mode 100644 index 000000000..6d1c6ed2f --- /dev/null +++ b/lib/inwx/inwx.php @@ -0,0 +1,155 @@ +username = $username; + $this->password = $password; + } + + /** + * Get the list of contacts. + * + * @param int $pageLimit + * @return array + * @throws Exception + */ + public function contactList(int $pageLimit = 300): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'contact.list', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'pagelimit' => $pageLimit, + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData)['contact']; + } + + /** + * Get the list of domains. + * + * @param int $pageLimit + * @return array + * @throws Exception + */ + public function domainList(int $pageLimit = 300): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'domain.list', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'pagelimit' => $pageLimit, + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData)['domain']; + } + + /** + * Check if a domain is available. + * + * @param string $domain + * @param string $tld + * @return array + * @throws Exception + */ + public function domainCheck(string $domain): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'domain.check', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'domain' => $domain + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData); + } + + /** + * Get the price of a domain. + * + * @param string $domain + * @param string $priceType reg | renewal | transfer | update | trade | restore + * @return array + * @throws Exception + */ + public function domainGetDomainPrice(string $domain, string $priceType): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'domain.getdomainprice', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'domain' => $domain, + 'pricetype' => $priceType, + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData); + } + + /** + * Make a request to the INWX API. + * + * @param array $requestData + * @return array + * @throws Exception + */ + private function makeRequest(array $requestData): array { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $this->apiUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData)); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Accept: application/json' + )); + + $response = curl_exec($ch); + + if ($response === false) { + throw new Exception('cURL Error: ' . curl_error($ch)); + } + + curl_close($ch); + + $responseData = json_decode($response, true); + + if (isset($responseData['error'])) { + throw new Exception('JSON-RPC Error: ' . $responseData['error']['message']); + } + + if (!isset($responseData['resData'])) { + throw new Exception('Unexpected response format.'); + } + + return $responseData['resData']; + } +} +?> diff --git a/lib/plesk/plesk.php b/lib/plesk/plesk.php new file mode 100644 index 000000000..d0406d99f --- /dev/null +++ b/lib/plesk/plesk.php @@ -0,0 +1,36 @@ +host = $host; + $this->authorization = $authorization; + } + + public function getAllDomains() { + // Implement code to fetch all configured domains using Plesk API + // You can use cURL or any HTTP client library to make API requests + // Example: + $url = "https://{$this->host}/api/v2/domains"; + $headers = array( + "Authorization: {$this->authorization}", + "Content-Type: application/json" + ); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($curl); + + $response = json_decode($response, true); + + if (is_array($response)) { + return $response; + } else { + return false; + } + } +} \ No newline at end of file