From 190c95bacae679b68265214166c5256045ac6c7e Mon Sep 17 00:00:00 2001 From: "Michael Kaufmann (d00p)" Date: Mon, 19 Mar 2018 21:25:23 +0100 Subject: [PATCH] created DomainZones ApiCommand Signed-off-by: Michael Kaufmann (d00p) --- dns_editor.php | 256 ++----------- .../api/commands/class.DomainZones.php | 342 ++++++++++++++++++ tests/Extras/DirProtectionsTest.php | 2 +- 3 files changed, 372 insertions(+), 228 deletions(-) create mode 100644 lib/classes/api/commands/class.DomainZones.php diff --git a/dns_editor.php b/dns_editor.php index cd720fdc..7099d0cc 100644 --- a/dns_editor.php +++ b/dns_editor.php @@ -45,234 +45,31 @@ $success_message = ""; // action for adding a new entry if ($action == 'add_record' && ! empty($_POST)) { - - // validation - if (empty($record)) { - $record = "@"; - } - - $record = strtolower($record); - - if ($record != '@' && $record != '*') { - // validate record - if (strpos($record, '--') !== false) { - $errors[] = $lng['error']['domain_nopunycode']; - } else { - // check for wildcard-record - $add_wildcard_again = false; - if (substr($record, 0, 2) == '*.') { - $record = substr($record, 2); - $add_wildcard_again = true; - } - // convert entry - $record = $idna_convert->encode($record); - - if ($add_wildcard_again) { - $record = '*.'.$record; - } - - /* - * see https://redmine.froxlor.org/issues/1697 - * - if ($type != 'SRV' && $type != 'TXT') { - $check_dom = $record . '.example.com'; - if (! validateDomain($check_dom)) { - $errors[] = sprintf($lng['error']['subdomainiswrong'], $idna_convert->decode($record)); - } - } - */ - if (strlen($record) > 63) { - $errors[] = $lng['error']['dns_record_toolong']; - } - } - } - - // TODO regex validate content for invalid characters - - if ($ttl <= 0) { - $ttl = 18000; - } - - if (empty($content)) { - $errors[] = $lng['error']['dns_content_empty']; - } - - // types - if ($type == 'A' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) { - $errors[] = $lng['error']['dns_arec_noipv4']; - } elseif ($type == 'AAAA' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) { - $errors[] = $lng['error']['dns_aaaarec_noipv6']; - } elseif ($type == 'MX') { - if ($prio === null || $prio < 0) { - $errors[] = $lng['error']['dns_mx_prioempty']; - } - // check for trailing dot - if (substr($content, - 1) == '.') { - // remove it for checks - $content = substr($content, 0, - 1); - } - if (! validateDomain($content)) { - $errors[] = $lng['error']['dns_mx_needdom']; - } else { - // check whether there is a CNAME-record for the same resource - foreach ($dom_entries as $existing_entries) { - $fqdn = $existing_entries['record'] . '.' . $domain; - if ($existing_entries['type'] == 'CNAME' && $fqdn == $content) { - $errors[] = $lng['error']['dns_mx_noalias']; - break; - } - } - } - // append trailing dot (again) - $content .= '.'; - } elseif ($type == 'CNAME') { - // check for trailing dot - if (substr($content, - 1) == '.') { - // remove it for checks - $content = substr($content, 0, - 1); - } else { - // add domain name - $content .= '.' . $domain; - } - if (! validateDomain($content)) { - $errors[] = $lng['error']['dns_cname_invaliddom']; - } else { - // check whether there are RR-records for the same resource - foreach ($dom_entries as $existing_entries) { - if (($existing_entries['type'] == 'A' || $existing_entries['type'] == 'AAAA' || $existing_entries['type'] == 'MX' || $existing_entries['type'] == 'NS') && $existing_entries['record'] == $record) { - $errors[] = $lng['error']['dns_cname_nomorerr']; - break; - } - } - } - // append trailing dot (again) - $content .= '.'; - } elseif ($type == 'NS') { - // check for trailing dot - if (substr($content, - 1) == '.') { - // remove it for checks - $content = substr($content, 0, - 1); - } - if (! validateDomain($content)) { - $errors[] = $lng['error']['dns_ns_invaliddom']; - } - // append trailing dot (again) - $content .= '.'; - } elseif ($type == 'TXT' && ! empty($content)) { - // check that TXT content is enclosed in " " - $content = encloseTXTContent($content); - } elseif ($type == 'SRV') { - if ($prio === null || $prio < 0) { - $errors[] = $lng['error']['dns_srv_prioempty']; - } - // check only last part of content, as it can look like: - // _service._proto.name. TTL class SRV priority weight port target. - $_split_content = explode(" ", $content); - // SRV content must be [weight] [port] [target] - if (count($_split_content) != 3) { - $errors[] = $lng['error']['dns_srv_invalidcontent']; - } - $target = trim($_split_content[count($_split_content) - 1]); - if ($target != '.') { - // check for trailing dot - if (substr($target, - 1) == '.') { - // remove it for checks - $target = substr($target, 0, - 1); - } - } - if ($target != '.' && ! validateDomain($target)) { - $errors[] = $lng['error']['dns_srv_needdom']; - } else { - // check whether there is a CNAME-record for the same resource - foreach ($dom_entries as $existing_entries) { - $fqdn = $existing_entries['record'] . '.' . $domain; - if ($existing_entries['type'] == 'CNAME' && $fqdn == $target) { - $errors[] = $lng['error']['dns_srv_noalias']; - break; - } - } - } - // append trailing dot if there's none - if (substr($content, - 1) != '.') { - $content .= '.'; - } - } - - $new_entry = array( - 'record' => $record, - 'type' => $type, - 'prio' => $prio, - 'content' => $content, - 'ttl' => $ttl, - 'domain_id' => $domain_id - ); - ksort($new_entry); - - // check for duplicate - foreach ($dom_entries as $existing_entry) { - // compare serialized string of array - $check_entry = $existing_entry; - // new entry has no ID yet - unset($check_entry['id']); - // sort by key - ksort($check_entry); - // format integer fields to real integer (as they are read as string from the DB) - $check_entry['prio'] = (int) $check_entry['prio']; - $check_entry['ttl'] = (int) $check_entry['ttl']; - $check_entry['domain_id'] = (int) $check_entry['domain_id']; - // serialize both - $check_entry = serialize($check_entry); - $new = serialize($new_entry); - // compare - if ($check_entry === $new) { - $errors[] = $lng['error']['dns_duplicate_entry']; - unset($check_entry); - break; - } - } - - if (empty($errors)) { - $ins_stmt = Database::prepare(" - INSERT INTO `" . TABLE_DOMAIN_DNS . "` SET - `record` = :record, - `type` = :type, - `prio` = :prio, - `content` = :content, - `ttl` = :ttl, - `domain_id` = :domain_id - "); - - Database::pexecute($ins_stmt, $new_entry); - - $new_entry_id = Database::lastInsertId(); - - // add temporary to the entries-array (no reread of DB necessary) - $new_entry['id'] = $new_entry_id; - $dom_entries[] = $new_entry; - - // success message (inline) + try { + DomainZones::getLocal($userinfo, array( + 'id' => $domain_id, + 'record' => $record, + 'type' => $type, + 'prio' => $prio, + 'content' => $content, + 'ttl' => $ttl + ))->add(); $success_message = $lng['success']['dns_record_added']; - - $record = ""; - $type = 'A'; - $prio = ""; - $content = ""; - $ttl = ""; - - // re-generate bind configs - inserttask('4'); - } else { - // show $errors - $errors = implode("
", $errors); + } catch (Exception $e) { + dynamic_error($e->getMessage()); } } elseif ($action == 'delete') { // remove entry $entry_id = isset($_GET['id']) ? (int) $_GET['id'] : 0; if ($entry_id > 0) { - $del_stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `id` = :id"); - Database::pexecute($del_stmt, array( - 'id' => $entry_id - )); + try { + DomainZones::getLocal($userinfo, array( + 'entry_id' => $entry_id, + 'id' => $domain_id + ))->delete(); + } catch (Exception $e) { + dynamic_error($e->getMessage()); + } // remove deleted entry from internal data array (no reread of DB necessary) $_t = $dom_entries; @@ -285,9 +82,6 @@ if ($action == 'add_record' && ! empty($_POST)) { unset($_t); // success message (inline) $success_message = $lng['success']['dns_record_deleted']; - - // re-generate bind configs - inserttask('4'); } } @@ -322,6 +116,14 @@ foreach ($type_select_values as $_type) { eval("\$record_list=\"" . getTemplate("dns_editor/list", true) . "\";"); -$zone = createDomainZone($domain_id); -$zonefile = (string) $zone; +try { + $json_result = DomainZones::getLocal($userinfo, array( + 'id' => $domain_id + ))->get(); +} catch (Exception $e) { + dynamic_error($e->getMessage()); +} +$result = json_decode($json_result, true)['data']; +$zonefile = implode("\n", $result); + eval("echo \"" . getTemplate("dns_editor/index", true) . "\";"); diff --git a/lib/classes/api/commands/class.DomainZones.php b/lib/classes/api/commands/class.DomainZones.php new file mode 100644 index 00000000..dbdacad6 --- /dev/null +++ b/lib/classes/api/commands/class.DomainZones.php @@ -0,0 +1,342 @@ + (2010-) + * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt + * @package API + * @since 0.10.0 + * + */ +class DomainZones extends ApiCommand implements ResourceEntity +{ + + public function add() + { + if (Settings::Get('system.dnsenabled') != '1') { + throw new Exception("DNS server not enabled on this system", 405); + } + + $id = $this->getParam('id', true, 0); + $dn_optional = ($id <= 0 ? false : true); + $domainname = $this->getParam('domainname', $dn_optional, ''); + + // get requested domain + $result = $this->apiCall('SubDomains.get', array( + 'id' => $id, + 'domainname' => $domainname + )); + $id = $result['id']; + + // parameters + $record = $this->getParam('record', true, null); + $type = $this->getParam('type', true, 'A'); + $prio = $this->getParam('prio', true, null); + $content = $this->getParam('content', true, null); + $ttl = $this->getParam('ttl', true, 18000); + + // validation + if ($result['isbinddomain'] != '1') { + standard_error('dns_domain_nodns', '', true); + } + + $idna_convert = new idna_convert_wrapper(); + $domain = $idna_convert->encode($result['domain']); + + // select all entries + $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE domain_id = :did"); + Database::pexecute($sel_stmt, array( + 'did' => $id + ), true, true); + $dom_entries = $sel_stmt->fetchAll(PDO::FETCH_ASSOC); + + // validation + if (empty($record)) { + $record = "@"; + } + + $record = trim(strtolower($record)); + + if ($record != '@' && $record != '*') { + // validate record + if (strpos($record, '--') !== false) { + $errors[] = $this->lng['error']['domain_nopunycode']; + } else { + // check for wildcard-record + $add_wildcard_again = false; + if (substr($record, 0, 2) == '*.') { + $record = substr($record, 2); + $add_wildcard_again = true; + } + // convert entry + $record = $idna_convert->encode($record); + + if ($add_wildcard_again) { + $record = '*.' . $record; + } + + if (strlen($record) > 63) { + $errors[] = $this->lng['error']['dns_record_toolong']; + } + } + } + + // TODO regex validate content for invalid characters + + if ($ttl <= 0) { + $ttl = 18000; + } + + $content = trim($content); + if (empty($content)) { + $errors[] = $this->lng['error']['dns_content_empty']; + } + + // types + if ($type == 'A' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) { + $errors[] = $this->lng['error']['dns_arec_noipv4']; + } elseif ($type == 'AAAA' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) { + $errors[] = $this->lng['error']['dns_aaaarec_noipv6']; + } elseif ($type == 'MX') { + if ($prio === null || $prio < 0) { + $errors[] = $this->lng['error']['dns_mx_prioempty']; + } + // check for trailing dot + if (substr($content, - 1) == '.') { + // remove it for checks + $content = substr($content, 0, - 1); + } + if (! validateDomain($content)) { + $errors[] = $this->lng['error']['dns_mx_needdom']; + } else { + // check whether there is a CNAME-record for the same resource + foreach ($dom_entries as $existing_entries) { + $fqdn = $existing_entries['record'] . '.' . $domain; + if ($existing_entries['type'] == 'CNAME' && $fqdn == $content) { + $errors[] = $this->lng['error']['dns_mx_noalias']; + break; + } + } + } + // append trailing dot (again) + $content .= '.'; + } elseif ($type == 'CNAME') { + // check for trailing dot + if (substr($content, - 1) == '.') { + // remove it for checks + $content = substr($content, 0, - 1); + } else { + // add domain name + $content .= '.' . $domain; + } + if (! validateDomain($content)) { + $errors[] = $this->lng['error']['dns_cname_invaliddom']; + } else { + // check whether there are RR-records for the same resource + foreach ($dom_entries as $existing_entries) { + if (($existing_entries['type'] == 'A' || $existing_entries['type'] == 'AAAA' || $existing_entries['type'] == 'MX' || $existing_entries['type'] == 'NS') && $existing_entries['record'] == $record) { + $errors[] = $this->lng['error']['dns_cname_nomorerr']; + break; + } + } + } + // append trailing dot (again) + $content .= '.'; + } elseif ($type == 'NS') { + // check for trailing dot + if (substr($content, - 1) == '.') { + // remove it for checks + $content = substr($content, 0, - 1); + } + if (! validateDomain($content)) { + $errors[] = $this->lng['error']['dns_ns_invaliddom']; + } + // append trailing dot (again) + $content .= '.'; + } elseif ($type == 'TXT' && ! empty($content)) { + // check that TXT content is enclosed in " " + $content = encloseTXTContent($content); + } elseif ($type == 'SRV') { + if ($prio === null || $prio < 0) { + $errors[] = $this->lng['error']['dns_srv_prioempty']; + } + // check only last part of content, as it can look like: + // _service._proto.name. TTL class SRV priority weight port target. + $_split_content = explode(" ", $content); + // SRV content must be [weight] [port] [target] + if (count($_split_content) != 3) { + $errors[] = $this->lng['error']['dns_srv_invalidcontent']; + } + $target = trim($_split_content[count($_split_content) - 1]); + if ($target != '.') { + // check for trailing dot + if (substr($target, - 1) == '.') { + // remove it for checks + $target = substr($target, 0, - 1); + } + } + if ($target != '.' && ! validateDomain($target)) { + $errors[] = $this->lng['error']['dns_srv_needdom']; + } else { + // check whether there is a CNAME-record for the same resource + foreach ($dom_entries as $existing_entries) { + $fqdn = $existing_entries['record'] . '.' . $domain; + if ($existing_entries['type'] == 'CNAME' && $fqdn == $target) { + $errors[] = $this->lng['error']['dns_srv_noalias']; + break; + } + } + } + // append trailing dot if there's none + if (substr($content, - 1) != '.') { + $content .= '.'; + } + } + + $new_entry = array( + 'record' => $record, + 'type' => $type, + 'prio' => $prio, + 'content' => $content, + 'ttl' => $ttl, + 'domain_id' => $id + ); + ksort($new_entry); + + // check for duplicate + foreach ($dom_entries as $existing_entry) { + // compare serialized string of array + $check_entry = $existing_entry; + // new entry has no ID yet + unset($check_entry['id']); + // sort by key + ksort($check_entry); + // format integer fields to real integer (as they are read as string from the DB) + $check_entry['prio'] = (int) $check_entry['prio']; + $check_entry['ttl'] = (int) $check_entry['ttl']; + $check_entry['domain_id'] = (int) $check_entry['domain_id']; + // serialize both + $check_entry = serialize($check_entry); + $new = serialize($new_entry); + // compare + if ($check_entry === $new) { + $errors[] = $this->lng['error']['dns_duplicate_entry']; + unset($check_entry); + break; + } + } + + if (empty($errors)) { + $ins_stmt = Database::prepare(" + INSERT INTO `" . TABLE_DOMAIN_DNS . "` SET + `record` = :record, + `type` = :type, + `prio` = :prio, + `content` = :content, + `ttl` = :ttl, + `domain_id` = :domain_id + "); + Database::pexecute($ins_stmt, $new_entry, true, true); + $new_entry_id = Database::lastInsertId(); + + // add temporary to the entries-array (no reread of DB necessary) + $new_entry['id'] = $new_entry_id; + $dom_entries[] = $new_entry; + + // re-generate bind configs + inserttask('4'); + + $result = $this->apiCall('DomainZones.get', array( + 'id' => $id + )); + return $this->response(200, "successfull", $result); + } + // return $errors + throw new Exception(implode("\n", $errors)); + } + + /** + * return a domain-dns entry by either id or domainname + * + * @param int $id + * optional, the domain-id + * @param string $domainname + * optional, the domainname + * + * @access admin + * @throws Exception + * @return array + */ + public function get() + { + if (Settings::Get('system.dnsenabled') != '1') { + throw new Exception("DNS server not enabled on this system", 405); + } + + $id = $this->getParam('id', true, 0); + $dn_optional = ($id <= 0 ? false : true); + $domainname = $this->getParam('domainname', $dn_optional, ''); + + // get requested domain + $result = $this->apiCall('SubDomains.get', array( + 'id' => $id, + 'domainname' => $domainname + )); + $id = $result['id']; + + if ($result['isbinddomain'] != '1') { + standard_error('dns_domain_nodns', '', true); + } + + $zone = createDomainZone($id); + $zonefile = (string) $zone; + + $this->logger()->logAction($this->isAdmin() ? ADM_ACTION : USR_ACTION, LOG_NOTICE, "[API] get dns-zone for '" . $result['domain'] . "'"); + return $this->response(200, "successfull", explode("\n", $zonefile)); + } + + public function update() + { + throw new Exception('You cannot update a dns zone entry. You need to delete it and re-add it.', 303); + } + + public function listing() + { + throw new Exception('You cannot list dns zones. To get all domains use Domains.listing() or SubDomains.listing()', 303); + } + + public function delete() + { + if (Settings::Get('system.dnsenabled') != '1') { + throw new Exception("DNS server not enabled on this system", 405); + } + + $entry_id = $this->getParam('entry_id'); + $id = $this->getParam('id', true, 0); + $dn_optional = ($id <= 0 ? false : true); + $domainname = $this->getParam('domainname', $dn_optional, ''); + + // get requested domain + $result = $this->apiCall('SubDomains.get', array( + 'id' => $id, + 'domainname' => $domainname + )); + $id = $result['id']; + + $del_stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `id` = :id AND `domain_id` = :did"); + Database::pexecute($del_stmt, array( + 'id' => $entry_id, + 'did' => $id + ), true, true); + // re-generate bind configs + inserttask('4'); + return $this->response(200, "successfull", true); + } +} diff --git a/tests/Extras/DirProtectionsTest.php b/tests/Extras/DirProtectionsTest.php index f4046143..23c440b6 100644 --- a/tests/Extras/DirProtectionsTest.php +++ b/tests/Extras/DirProtectionsTest.php @@ -98,7 +98,7 @@ class DirProtectionsTest extends TestCase public function testResellerDirProtectionsGet() { global $admin_userdata; - // get customer + // get reseller $json_result = Admins::getLocal($admin_userdata, array( 'loginname' => 'reseller' ))->get();