From 1db242f3cd8037c63238ab98b5f85d9b4eae4bfd Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:27:41 +0000 Subject: [PATCH 01/13] Fix cURL error handling, add timeouts, and harden API response parsing - Add CURLOPT_CONNECTTIMEOUT (30s) and CURLOPT_TIMEOUT (60s) to prevent hanging requests on unresponsive API endpoints - Return empty OpensrsResponse instead of passing false to constructor when curl_exec fails, preventing downstream type errors - Fix $siganture typo to $signature (cosmetic, no functional impact as the variable was used consistently) - Escape XML attribute values with htmlspecialchars() to prevent malformed XML from special characters in domain contact data - Make raw() null-safe with ?? '' fallback - Cache formatResponse() result in errors() to avoid 3 redundant parses - Return 'ERROR' instead of null from status() when XML is missing, so callers can safely compare without null checks --- apis/opensrs_api.php | 13 +++++++++---- apis/opensrs_response.php | 18 +++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apis/opensrs_api.php b/apis/opensrs_api.php index 246d36b..2b1d143 100644 --- a/apis/opensrs_api.php +++ b/apis/opensrs_api.php @@ -87,11 +87,11 @@ public function submit(string $command, array $args = [], string $object = 'doma } // Build signature - $siganture = md5($xml_request . $this->key); + $signature = md5($xml_request . $this->key); $headers = [ 'Content-Type: text/xml', 'X-Username: ' . trim($this->username), - 'X-Signature: ' . md5($siganture . $this->key), + 'X-Signature: ' . md5($signature . $this->key), 'Content-Length: ' . strlen($xml_request) ]; @@ -109,6 +109,8 @@ public function submit(string $command, array $args = [], string $object = 'doma curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml_request); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); if (Configure::get('Blesta.curl_verify_ssl')) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); @@ -120,8 +122,11 @@ public function submit(string $command, array $args = [], string $object = 'doma $response = curl_exec($ch); - if ($response == false) { + if ($response === false) { $this->logger->error(curl_error($ch)); + curl_close($ch); + + return new OpensrsResponse(''); } curl_close($ch); @@ -186,7 +191,7 @@ private function buildRecursiveAttributes(SimpleXMLElement &$dt_assoc, array $ar $this->buildRecursiveAttributes($assoc, $value); } else { - $dt_assoc->addChild('item', $value) + $dt_assoc->addChild('item', htmlspecialchars((string) $value, ENT_XML1, 'UTF-8')) ->addAttribute('key', $key); } } diff --git a/apis/opensrs_response.php b/apis/opensrs_response.php index f89293d..e43e2b4 100755 --- a/apis/opensrs_response.php +++ b/apis/opensrs_response.php @@ -53,13 +53,13 @@ public function response() : ?object * * @return string The status (OK = success, ERROR = error, null = invalid responses) */ - public function status() : ?string + public function status() : string { if ($this->xml && $this->xml instanceof SimpleXMLElement) { return ($this->formatResponse($this->xml->body->data_block)->is_success ?? false) ? 'OK' : 'ERROR'; } - return null; + return 'ERROR'; } /** @@ -70,18 +70,18 @@ public function status() : ?string public function errors() : ?object { if ($this->xml && $this->xml instanceof SimpleXMLElement) { - $error = 'Internal Server Error'; + $data = $this->formatResponse($this->xml->body->data_block); - $error_msg = $this->formatResponse($this->xml->body->data_block)->attributes['error'] - ?? $this->formatResponse($this->xml->body->data_block)->response_text - ?? $error; + $error_msg = $data->attributes['error'] + ?? $data->response_text + ?? 'Internal Server Error'; return (object)[ 'response_text' => $error_msg, - 'response_code' => $this->formatResponse($this->xml->body->data_block)->response_code ?? 500 + 'response_code' => $data->response_code ?? 500 ]; } - + return null; } @@ -92,7 +92,7 @@ public function errors() : ?object */ public function raw() : string { - return $this->raw; + return $this->raw ?? ''; } /** From 45ee783319688ddfbb42b2d0d83f153275706164 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:28:39 +0000 Subject: [PATCH 02/13] Fix processResponse null handling and add early returns for API failures - Handle null from response->errors() gracefully with fallback message - Add early returns after processResponse() in getDomainContacts, getDomainInfo, getDomainIsLocked, getDomainIsPrivate, and getDomainNameServers to prevent dereferencing null responses - Add null-coalescing for attribute access (lock_state, state) to prevent undefined index errors - Replace gethostbyname() fallback with empty string for missing IPs to avoid DNS lookups that could hang or return wrong data - Guard nameserver_list iteration with ?? [] for missing key - Fix addService() blanket Input::setErrors([]) that was clearing real registration errors along with nameserver errors - Filter empty nameservers before passing to setDomainNameservers --- opensrs.php | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/opensrs.php b/opensrs.php index ab0c33a..287485c 100644 --- a/opensrs.php +++ b/opensrs.php @@ -437,15 +437,20 @@ public function addService( } // Set nameservers - $this->setDomainNameservers($vars['domain'], $package->module_row, [ + $this->setDomainNameservers($vars['domain'], $package->module_row, array_filter([ $fields['nameserver_list'][0]['name'] ?? '', $fields['nameserver_list'][1]['name'] ?? '', $fields['nameserver_list'][2]['name'] ?? '', $fields['nameserver_list'][3]['name'] ?? '', - ]); + ])); - // Ignore nameserver errors - $this->Input->setErrors([]); + // Clear nameserver errors only (registration already succeeded) + if ($this->Input->errors()) { + $errors = $this->Input->errors(); + if (isset($errors['errors']) && count($errors) === 1) { + $this->Input->setErrors([]); + } + } return [['key' => 'domain', 'value' => $vars['domain'], 'encrypted' => 0]]; } @@ -1366,6 +1371,10 @@ public function getDomainContacts($domain, $module_row_id = null) 'type' => 'all_info' ]); $this->processResponse($api, $response); + + if ($response->status() != 'OK') { + return []; + } $response = $response->response(); $contacts = $response->attributes['contact_set'] ?? []; @@ -1447,6 +1456,10 @@ public function getDomainInfo($domain, $module_row_id = null) 'type' => 'all_info' ]); $this->processResponse($api, $response); + + if ($response->status() != 'OK') { + return []; + } $response = $response->response(); return $response->attributes ?? []; @@ -1470,9 +1483,13 @@ public function getDomainIsLocked($domain, $module_row_id = null) 'type' => 'status' ]); $this->processResponse($api, $response); + + if ($response->status() != 'OK') { + return false; + } $response = $response->response(); - return $response->attributes['lock_state'] == '1'; + return ($response->attributes['lock_state'] ?? '0') == '1'; } /** @@ -1493,9 +1510,13 @@ private function getDomainIsPrivate($domain, $module_row_id = null) 'type' => 'whois_privacy_state' ]); $this->processResponse($api, $response); + + if ($response->status() != 'OK') { + return false; + } $response = $response->response(); - return $response->attributes['state'] == 'enabled'; + return ($response->attributes['state'] ?? 'disabled') == 'enabled'; } /** @@ -1513,10 +1534,10 @@ public function getDomainNameServers($domain, $module_row_id = null) $domain_info = $this->getDomainInfo($domain, $module_row_id); $nameservers = []; - foreach ($domain_info['nameserver_list'] as $nameserver) { + foreach ($domain_info['nameserver_list'] ?? [] as $nameserver) { $nameservers[] = [ 'url' => $nameserver['name'] ?? '', - 'ips' => [$nameserver['ipaddress'] ?? gethostbyname($nameserver['name'] ?? '')] + 'ips' => [$nameserver['ipaddress'] ?? ''] ]; } @@ -1889,7 +1910,8 @@ private function processResponse(OpensrsApi $api, OpensrsResponse $response) // Set errors, if any if ($response->status() != 'OK') { - $errors = isset($response->errors()->response_text) ? $response->errors()->response_text : ''; + $error_obj = $response->errors(); + $errors = $error_obj->response_text ?? 'An unknown error occurred'; $this->Input->setErrors(['errors' => [$errors]]); } } From 09991d6dbec0406fc937207878ee64fdceb1462e Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:31:05 +0000 Subject: [PATCH 03/13] Add null checks for getModuleRow to prevent fatal errors - Add getModuleRowOrFail() helper that sets a user-friendly error when the module row cannot be found, instead of causing a fatal PHP error from accessing properties on null - Replace all 15 direct getModuleRow() calls with getModuleRowOrFail() plus early returns in: getFilteredTldPricing, manageSettings, checkAvailability, getDomainContacts, setDomainContacts, getDomainInfo, getDomainIsLocked, getDomainIsPrivate, setDomainNameservers, lockDomain, unlockDomain, registerDomain, renewDomain, getExpirationDate, getRegistrationDate - Rename misleading $domains_provisioning to $domains_ns in setDomainNameservers (it was instantiating OpensrsDomainsNs) - Add language key for module_row.missing error --- language/en_us/opensrs.php | 1 + opensrs.php | 100 ++++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/language/en_us/opensrs.php b/language/en_us/opensrs.php index 122de8b..cbb81bf 100755 --- a/language/en_us/opensrs.php +++ b/language/en_us/opensrs.php @@ -73,6 +73,7 @@ $lang['Opensrs.!error.user.valid'] = 'Please enter a user'; $lang['Opensrs.!error.key.valid'] = 'Please enter a key'; $lang['Opensrs.!error.key.valid_connection'] = 'The user and key combination appear to be invalid, or your Opensrs account may not be configured to allow API access.'; +$lang['Opensrs.!error.module_row.missing'] = 'The module row could not be found. Please reconfigure the module.'; $lang['Opensrs.!error.registrant_type.format'] = 'Please select a registrant type'; $lang['Opensrs.!error.registrant_vat_id.format'] = 'Please enter a VAT ID'; $lang['Opensrs.!error.siren_siret.format'] = 'Please enter a SIREN/SIRET Number'; diff --git a/opensrs.php b/opensrs.php index 287485c..967f1af 100644 --- a/opensrs.php +++ b/opensrs.php @@ -82,7 +82,10 @@ public function getFilteredTldPricing($module_row_id = null, $filters = []) // Fetch pricing from the registrar - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return []; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1248,7 +1251,10 @@ private function manageSettings( // Load the helpers required for this view Loader::loadHelpers($this, ['Form', 'Html']); - $row = $this->getModuleRow($package->module_row); + $row = $this->getModuleRowOrFail($package->module_row); + if (!$row) { + return ''; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $vars = new stdClass(); @@ -1312,7 +1318,10 @@ private function manageSettings( */ public function checkAvailability($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1362,7 +1371,10 @@ public function checkTransferAvailability($domain, $module_row_id = null) */ public function getDomainContacts($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return []; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1411,7 +1423,10 @@ public function getDomainContacts($domain, $module_row_id = null) */ public function setDomainContacts($domain, array $vars = [], $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains_provisioning = new OpensrsDomainsProvisioning($api); @@ -1447,7 +1462,10 @@ public function setDomainContacts($domain, array $vars = [], $module_row_id = nu */ public function getDomainInfo($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return []; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1474,7 +1492,10 @@ public function getDomainInfo($domain, $module_row_id = null) */ public function getDomainIsLocked($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1501,7 +1522,10 @@ public function getDomainIsLocked($domain, $module_row_id = null) */ private function getDomainIsPrivate($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1554,10 +1578,13 @@ public function getDomainNameServers($domain, $module_row_id = null) */ public function setDomainNameservers($domain, $module_row_id = null, array $vars = []) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); - $domains_provisioning = new OpensrsDomainsNs($api); + $domains_ns = new OpensrsDomainsNs($api); // Remove empty nameservers foreach ($vars as $key => $ns) { @@ -1567,7 +1594,7 @@ public function setDomainNameservers($domain, $module_row_id = null, array $vars } // Update domain - $response = $domains_provisioning->advancedUpdateNameserver([ + $response = $domains_ns->advancedUpdateNameserver([ 'domain' => $domain, 'op_type' => 'assign', 'assign_ns' => array_values($vars) @@ -1586,7 +1613,10 @@ public function setDomainNameservers($domain, $module_row_id = null, array $vars */ public function lockDomain($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains_provisioning = new OpensrsDomainsProvisioning($api); @@ -1612,7 +1642,10 @@ public function lockDomain($domain, $module_row_id = null) */ public function unlockDomain($domain, $module_row_id = null) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains_provisioning = new OpensrsDomainsProvisioning($api); @@ -1641,7 +1674,10 @@ public function unlockDomain($domain, $module_row_id = null) */ public function registerDomain($domain, $module_row_id = null, array $vars = []) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); // Set all whois info from client @@ -1715,7 +1751,10 @@ public function transferDomain($domain, $module_row_id = null, array $vars = []) */ public function renewDomain($domain, $module_row_id = null, array $vars = []) { - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $params = [ @@ -1748,7 +1787,10 @@ public function getExpirationDate($service, $format = 'Y-m-d H:i:s') $domain = $this->getServiceDomain($service); $module_row_id = $service->module_row_id ?? null; - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1782,7 +1824,10 @@ public function getRegistrationDate($service, $format = 'Y-m-d H:i:s') $domain = $this->getServiceDomain($service); $module_row_id = $service->module_row_id ?? null; - $row = $this->getModuleRow($module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); $domains = new OpensrsDomains($api); @@ -1883,6 +1928,27 @@ public function validateConnection($key, $user, $sandbox) return $response->is_success == '1'; } + /** + * Gets a module row, setting errors if not found + * + * @param int|null $module_row_id The module row ID + * @return stdClass|null The module row, or null if not found + */ + private function getModuleRowOrFail($module_row_id) + { + $row = $this->getModuleRow($module_row_id); + + if (!$row) { + $this->Input->setErrors(['errors' => [ + Language::_('Opensrs.!error.module_row.missing', true) + ]]); + + return null; + } + + return $row; + } + /** * Initializes the OpensrsApi and returns an instance of that object * From 56d2b141c39dc45909065cbaf9f06a8d106a7e5c Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:31:51 +0000 Subject: [PATCH 04/13] Fix registration security and input validation - Replace deterministic password (md5 of client ID) with cryptographically random password using random_bytes() - Add null-coalescing defaults for registrar_lock and whois_privacy_state POST values to prevent PHP notices - Add null check for $client before WHOIS fields loop in addService() to prevent fatal error when client_id is missing - Wrap validateConnection() in try-catch and add null safety for response properties to prevent crashes on invalid credentials --- opensrs.php | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/opensrs.php b/opensrs.php index 967f1af..2317019 100644 --- a/opensrs.php +++ b/opensrs.php @@ -367,12 +367,18 @@ public function addService( Loader::loadModels($this, ['Contacts']); } - $client = $this->Clients->get($vars['client_id']); + $client = $this->Clients->get($vars['client_id'] ?? null); if ($client) { $contact_numbers = $this->Contacts->getNumbers($client->contact_id); + } else { + $contact_numbers = []; } foreach ($whois_fields as $key => $value) { + if (!$client) { + $vars[$key] = 'NA'; + continue; + } if (str_contains($key, 'first_name')) { $vars[$key] = $client->first_name; } elseif (str_contains($key, 'last_name')) { @@ -1268,7 +1274,7 @@ private function manageSettings( if (!empty($post)) { // Set domain status - if ($post['registrar_lock'] == 'true') { + if (($post['registrar_lock'] ?? 'false') == 'true') { $this->lockDomain($fields->domain, $package->module_row); } else { $this->unlockDomain($fields->domain, $package->module_row); @@ -1280,7 +1286,7 @@ private function manageSettings( 'domain' => $fields->domain, 'data' => 'whois_privacy_state', 'affect_domains' => '0', - 'state' => $post['whois_privacy_state'] == 'true' ? 'Y' : 'N' + 'state' => ($post['whois_privacy_state'] ?? 'false') == 'true' ? 'Y' : 'N' ]); $this->processResponse($api, $response); @@ -1693,7 +1699,7 @@ public function registerDomain($domain, $module_row_id = null, array $vars = []) 'auto_renew' => 0, 'reg_type' => isset($vars['auth_info']) ? 'transfer' : 'new', 'reg_username' => 'usr' . ($client->id_value ?? $client->id ?? rand(10000, 99999)), - 'reg_password' => substr(base64_encode(md5($client->id_value)), 0, 15), + 'reg_password' => substr(bin2hex(random_bytes(10)), 0, 15), 'handle' => 'process' ]; $fields = array_merge($params, $vars); @@ -1921,11 +1927,21 @@ private function getRowRules(&$vars) */ public function validateConnection($key, $user, $sandbox) { - $api = $this->getApi($user, $key, $sandbox == 'true'); - $domains = new OpensrsDomains($api); - $response = $domains->lookup(['domain' => 'blesta.com'])->response(); + try { + $api = $this->getApi($user, $key, $sandbox == 'true'); + $domains = new OpensrsDomains($api); + $result = $domains->lookup(['domain' => 'blesta.com']); - return $response->is_success == '1'; + if ($result->status() != 'OK') { + return false; + } + + $response = $result->response(); + + return ($response->is_success ?? '0') == '1'; + } catch (Exception $e) { + return false; + } } /** From d785fd7c7eb3ab0c367df7298a43fc47d67debe6 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:32:30 +0000 Subject: [PATCH 05/13] Fix TLD pricing loops, .FR validation, and transfer availability check - Remove dead foreach(range(1,10)) loops wrapping pricing API calls; the API returns all periods in a single response via all_periods=1, so the loop just overwrote the same key 10 times - Fix .FR domain validation: unset company fields (VAT ID, SIREN, trademark) for 'individual' registrants, not 'organization' ones (the condition was inverted) - Replace naive checkTransferAvailability() inverse-lookup with proper OpensrsDomainsTransfer::checkTransfer() API call that checks the transferrable attribute, with fallback to old behavior --- opensrs.php | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/opensrs.php b/opensrs.php index 2317019..a83bdc8 100644 --- a/opensrs.php +++ b/opensrs.php @@ -109,9 +109,7 @@ public function getFilteredTldPricing($module_row_id = null, $filters = []) if ($register_response->status() != 'OK') { continue; } - foreach (range(1, 10) as $years) { - $pricing[$tld]['register'] = $register_response->response(); - } + $pricing[$tld]['register'] = $register_response->response(); // Set the renewal prices $renew_response = $domains->getPrice(array_merge($vars, ['reg_type' => 'renewal'])); @@ -119,9 +117,7 @@ public function getFilteredTldPricing($module_row_id = null, $filters = []) if ($renew_response->status() != 'OK') { continue; } - foreach (range(1, 10) as $years) { - $pricing[$tld]['renew'] = $renew_response->response(); - } + $pricing[$tld]['renew'] = $renew_response->response(); // Set the transfer prices $transfer_response = $domains->getPrice(array_merge($vars, ['reg_type' => 'transfer'])); @@ -129,9 +125,7 @@ public function getFilteredTldPricing($module_row_id = null, $filters = []) if ($transfer_response->status() != 'OK') { continue; } - foreach (range(1, 10) as $years) { - $pricing[$tld]['transfer'] = $transfer_response->response(); - } + $pricing[$tld]['transfer'] = $transfer_response->response(); } unset($tld); @@ -294,7 +288,7 @@ private function getServiceRules(array $vars = null, $edit = false) 'tld_data[registrant_extra_info][trademark_number]' ]; - if ($vars['tld_data']['registrant_extra_info']['registrant_type'] == 'organization') { + if ($vars['tld_data']['registrant_extra_info']['registrant_type'] == 'individual') { foreach ($company_fields as $field) { unset($rules[$field]); } @@ -1352,8 +1346,24 @@ public function checkAvailability($domain, $module_row_id = null) */ public function checkTransferAvailability($domain, $module_row_id = null) { - // Prevent users from transferring an unregistered domain - return !$this->checkAvailability($domain, $module_row_id); + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $transfers = new OpensrsDomainsTransfer($api); + $result = $transfers->checkTransfer(['domain' => $domain]); + $this->logRequest($api, $result); + + if ($result->status() != 'OK') { + // Fall back to inverse availability check + return !$this->checkAvailability($domain, $module_row_id); + } + + $response = $result->response(); + + return ($response->attributes['transferrable'] ?? '0') == '1'; } /** From 6a847e796a96d035b23ae717511d6929c4560023 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:33:33 +0000 Subject: [PATCH 06/13] Fix views, language key collisions, and minor UI issues - Mask API key in manage view, showing only last 4 characters - Change API key input to password field in add/edit row views - Remove unused $i variable in client WHOIS tab view - Namespace .FR domain language keys (fr_registrant_type) to prevent collision with .UK registrant_type keys that share the same key name - Update .FR config to reference namespaced language keys --- config/opensrs.php | 6 +++--- language/en_us/opensrs.php | 6 +++--- views/default/add_row.pdt | 2 +- views/default/edit_row.pdt | 2 +- views/default/manage.pdt | 2 +- views/default/tab_client_whois.pdt | 1 - 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/config/opensrs.php b/config/opensrs.php index c8a348e..6b293ec 100755 --- a/config/opensrs.php +++ b/config/opensrs.php @@ -1136,11 +1136,11 @@ // .FR Configure::set('Opensrs.domain_fields.fr', [ 'tld_data[registrant_extra_info][registrant_type]' => [ - 'label' => Language::_('Opensrs.domain.registrant_type', true), + 'label' => Language::_('Opensrs.domain.fr_registrant_type', true), 'type' => 'select', 'options' => [ - 'individual' => Language::_('Opensrs.domain.registrant_type.individual', true), - 'organization' => Language::_('Opensrs.domain.registrant_type.company', true), + 'individual' => Language::_('Opensrs.domain.fr_registrant_type.individual', true), + 'organization' => Language::_('Opensrs.domain.fr_registrant_type.organization', true), ] ], 'tld_data[registrant_extra_info][registrant_vat_id]' => [ diff --git a/language/en_us/opensrs.php b/language/en_us/opensrs.php index cbb81bf..8148d90 100755 --- a/language/en_us/opensrs.php +++ b/language/en_us/opensrs.php @@ -220,9 +220,9 @@ $lang['Opensrs.domain.id_number'] = 'Identity Number'; // .FR domain fields -$lang['Opensrs.domain.registrant_type'] = 'Legal Type'; -$lang['Opensrs.domain.registrant_type.individual'] = 'Individual'; -$lang['Opensrs.domain.registrant_type.organization'] = 'Company'; +$lang['Opensrs.domain.fr_registrant_type'] = 'Legal Type'; +$lang['Opensrs.domain.fr_registrant_type.individual'] = 'Individual'; +$lang['Opensrs.domain.fr_registrant_type.organization'] = 'Company'; $lang['Opensrs.domain.registrant_vat_id'] = 'VAT ID'; $lang['Opensrs.domain.siren_siret'] = 'SIREN/SIRET Number'; $lang['Opensrs.domain.trademark_number'] = 'Trademark Number'; diff --git a/views/default/add_row.pdt b/views/default/add_row.pdt index 2ebcf97..e70d4d9 100644 --- a/views/default/add_row.pdt +++ b/views/default/add_row.pdt @@ -20,7 +20,7 @@
  • Form->label($this->_('Opensrs.row_meta.key', true), 'key'); - $this->Form->fieldText('key', ($vars->key ?? null), ['id' => 'key']); + $this->Form->fieldPassword('key', ['id' => 'key', 'value' => ($vars->key ?? null)]); ?>
  • diff --git a/views/default/edit_row.pdt b/views/default/edit_row.pdt index 4fe3300..febc204 100644 --- a/views/default/edit_row.pdt +++ b/views/default/edit_row.pdt @@ -20,7 +20,7 @@
  • Form->label($this->_('Opensrs.row_meta.key', true), 'key'); - $this->Form->fieldText('key', ($vars->key ?? null), ['id' => 'key']); + $this->Form->fieldPassword('key', ['id' => 'key', 'value' => ($vars->key ?? null)]); ?>
  • diff --git a/views/default/manage.pdt b/views/default/manage.pdt index 80cca40..2f4c684 100644 --- a/views/default/manage.pdt +++ b/views/default/manage.pdt @@ -28,7 +28,7 @@ ?> > rows[$i]->meta->user) ? $this->Html->safe($module->rows[$i]->meta->user) : null);?> - rows[$i]->meta->key) ? $this->Html->safe($module->rows[$i]->meta->key) : null);?> + rows[$i]->meta->key) ? $this->Html->safe(str_repeat('*', max(0, strlen($module->rows[$i]->meta->key) - 4)) . substr($module->rows[$i]->meta->key, -4)) : null);?> _('Opensrs.row_meta.sandbox_' . (isset($module->rows[$i]->meta->sandbox) ? $module->rows[$i]->meta->sandbox : 'false'));?> _('Opensrs.manage.module_rows.edit');?> diff --git a/views/default/tab_client_whois.pdt b/views/default/tab_client_whois.pdt index a43e003..dd24e36 100644 --- a/views/default/tab_client_whois.pdt +++ b/views/default/tab_client_whois.pdt @@ -16,7 +16,6 @@
    $key) { ?>
    From 209a8d43737ea4b743964e3a13c0ce52c1df7784 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:34:26 +0000 Subject: [PATCH 07/13] Add cancelService, suspendService, unsuspendService, editService, restoreDomain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cancelService: Disables auto-renew and sets expire_action to let domain expire at end of current period via provisioning modify API - suspendService: Delegates to cancelService (standard Blesta pattern for domain modules — no OpenSRS suspend API exists) - unsuspendService: Re-enables auto-renew via provisioning modify - editService: Minimal implementation returning null to preserve existing service meta fields - restoreDomain: Wires up existing OpensrsDomainsProvisioning::redeem() method for domains in the redemption grace period - Add language keys for service lifecycle error messages --- language/en_us/opensrs.php | 4 + opensrs.php | 150 +++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/language/en_us/opensrs.php b/language/en_us/opensrs.php index 8148d90..cdd029e 100755 --- a/language/en_us/opensrs.php +++ b/language/en_us/opensrs.php @@ -74,6 +74,10 @@ $lang['Opensrs.!error.key.valid'] = 'Please enter a key'; $lang['Opensrs.!error.key.valid_connection'] = 'The user and key combination appear to be invalid, or your Opensrs account may not be configured to allow API access.'; $lang['Opensrs.!error.module_row.missing'] = 'The module row could not be found. Please reconfigure the module.'; +$lang['Opensrs.!error.cancel.failed'] = 'Failed to disable auto-renewal for this domain.'; +$lang['Opensrs.!error.suspend.failed'] = 'Failed to suspend this domain.'; +$lang['Opensrs.!error.unsuspend.failed'] = 'Failed to unsuspend this domain.'; +$lang['Opensrs.!error.restore.failed'] = 'Failed to restore this domain from redemption.'; $lang['Opensrs.!error.registrant_type.format'] = 'Please select a registrant type'; $lang['Opensrs.!error.registrant_vat_id.format'] = 'Please enter a VAT ID'; $lang['Opensrs.!error.siren_siret.format'] = 'Please enter a SIREN/SIRET Number'; diff --git a/opensrs.php b/opensrs.php index a83bdc8..044d22c 100644 --- a/opensrs.php +++ b/opensrs.php @@ -515,6 +515,156 @@ public function renewService($package, $service, $parent_package = null, $parent return null; } + /** + * Cancels the service on the remote server. Sets Input errors on failure, + * preventing the service from being canceled. + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param stdClass $parent_package A stdClass object representing the parent + * service's selected package (if the current service is an addon service) + * @param stdClass $parent_service A stdClass object representing the parent + * service of the service being canceled (if the current service is an addon service) + * @return mixed null to maintain the existing meta fields or a numerically + * indexed array of meta fields to be stored for this service containing: + * - key The key for this meta field + * - value The value for this key + * - encrypted Whether or not this field should be encrypted (default 0, not encrypted) + * @see Module::getModule() + * @see Module::getModuleRow() + */ + public function cancelService($package, $service, $parent_package = null, $parent_service = null) + { + $row = $this->getModuleRowOrFail($package->module_row); + if (!$row) { + return null; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $fields = $this->serviceFieldsToObject($service->fields); + + $domains_provisioning = new OpensrsDomainsProvisioning($api); + $response = $domains_provisioning->modify([ + 'domain' => $fields->domain, + 'data' => 'expire_action', + 'affect_domains' => '0', + 'auto_renew' => '0', + 'let_expire' => '1' + ]); + $this->processResponse($api, $response); + + return null; + } + + /** + * Suspends the service on the remote server. Sets Input errors on failure, + * preventing the service from being suspended. + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param stdClass $parent_package A stdClass object representing the parent + * service's selected package (if the current service is an addon service) + * @param stdClass $parent_service A stdClass object representing the parent + * service of the service being suspended (if the current service is an addon service) + * @return mixed null to maintain the existing meta fields or a numerically + * indexed array of meta fields to be stored for this service containing: + * - key The key for this meta field + * - value The value for this key + * - encrypted Whether or not this field should be encrypted (default 0, not encrypted) + * @see Module::getModule() + * @see Module::getModuleRow() + */ + public function suspendService($package, $service, $parent_package = null, $parent_service = null) + { + return $this->cancelService($package, $service, $parent_package, $parent_service); + } + + /** + * Unsuspends the service on the remote server. Sets Input errors on failure, + * preventing the service from being unsuspended. + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param stdClass $parent_package A stdClass object representing the parent + * service's selected package (if the current service is an addon service) + * @param stdClass $parent_service A stdClass object representing the parent + * service of the service being unsuspended (if the current service is an addon service) + * @return mixed null to maintain the existing meta fields or a numerically + * indexed array of meta fields to be stored for this service containing: + * - key The key for this meta field + * - value The value for this key + * - encrypted Whether or not this field should be encrypted (default 0, not encrypted) + * @see Module::getModule() + * @see Module::getModuleRow() + */ + public function unsuspendService($package, $service, $parent_package = null, $parent_service = null) + { + $row = $this->getModuleRowOrFail($package->module_row); + if (!$row) { + return null; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $fields = $this->serviceFieldsToObject($service->fields); + + $domains_provisioning = new OpensrsDomainsProvisioning($api); + $response = $domains_provisioning->modify([ + 'domain' => $fields->domain, + 'data' => 'expire_action', + 'affect_domains' => '0', + 'auto_renew' => '1', + 'let_expire' => '0' + ]); + $this->processResponse($api, $response); + + return null; + } + + /** + * Edits the service on the remote server. + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $vars An array of user supplied info to satisfy the request + * @param stdClass $parent_package A stdClass object representing the parent + * service's selected package (if the current service is an addon service) + * @param stdClass $parent_service A stdClass object representing the parent + * service of the service being edited (if the current service is an addon service) + * @return mixed null to maintain the existing meta fields or a numerically + * indexed array of meta fields to be stored for this service containing: + * - key The key for this meta field + * - value The value for this key + * - encrypted Whether or not this field should be encrypted (default 0, not encrypted) + * @see Module::getModule() + * @see Module::getModuleRow() + */ + public function editService($package, $service, array $vars = [], $parent_package = null, $parent_service = null) + { + return null; + } + + /** + * Restores a domain in the redemption grace period + * + * @param string $domain The domain to restore + * @param int $module_row_id The ID of the module row to fetch for the current module + * @return bool True if the domain was successfully restored, false otherwise + */ + public function restoreDomain($domain, $module_row_id = null) + { + $row = $this->getModuleRowOrFail($module_row_id); + if (!$row) { + return false; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $domains = new OpensrsDomainsProvisioning($api); + $response = $domains->redeem(['domain' => $domain]); + $this->processResponse($api, $response); + + return $response->status() == 'OK'; + } + /** * Validates input data when attempting to add a package, returns the meta * data to save when adding a package. Performs any action required to add From ba064d4265c042dce2544ced2f7aadfb11d1b131 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:36:21 +0000 Subject: [PATCH 08/13] Wire up DNS zone management tab with admin and client views - Register tabDns/tabClientDns in service tab arrays, conditional on dns_management feature being enabled for the service - Add manageDns() private method handling: - GET: Fetches DNS zone records via OpensrsDomainsDns::getDnsZone() - POST add_record: Appends new record to zone via setDnsZone() - POST delete_record: Removes record by type and index via setDnsZone() - POST reset_zone: Resets zone to defaults via resetDnsZone() - Create tab_dns.pdt (admin view) with record table, add form, and reset button - Create tab_client_dns.pdt (client view) with Bootstrap-styled table and form matching existing client tab patterns - Add all DNS-related language keys - The OpensrsDomainsDns class already existed but was never wired into the module UI --- language/en_us/opensrs.php | 13 +++ opensrs.php | 186 +++++++++++++++++++++++++++++++ views/default/tab_client_dns.pdt | 102 +++++++++++++++++ views/default/tab_dns.pdt | 117 +++++++++++++++++++ 4 files changed, 418 insertions(+) create mode 100644 views/default/tab_client_dns.pdt create mode 100644 views/default/tab_dns.pdt diff --git a/language/en_us/opensrs.php b/language/en_us/opensrs.php index cdd029e..905eebe 100755 --- a/language/en_us/opensrs.php +++ b/language/en_us/opensrs.php @@ -59,6 +59,19 @@ $lang['Opensrs.tab_nameservers.field_submit'] = 'Update Name Servers'; $lang['Opensrs.tab_settings.title'] = 'Settings'; + +$lang['Opensrs.tab_dns.title'] = 'DNS Records'; +$lang['Opensrs.tab_dns.field_type'] = 'Type'; +$lang['Opensrs.tab_dns.field_subdomain'] = 'Host'; +$lang['Opensrs.tab_dns.field_ip_address'] = 'Value'; +$lang['Opensrs.tab_dns.field_priority'] = 'Priority'; +$lang['Opensrs.tab_dns.field_ttl'] = 'TTL'; +$lang['Opensrs.tab_dns.field_options'] = 'Options'; +$lang['Opensrs.tab_dns.field_add'] = 'Add Record'; +$lang['Opensrs.tab_dns.field_delete'] = 'Delete'; +$lang['Opensrs.tab_dns.field_reset'] = 'Reset DNS Zone'; +$lang['Opensrs.tab_dns.add_record'] = 'Add DNS Record'; +$lang['Opensrs.tab_dns.no_records'] = 'There are no DNS records.'; $lang['Opensrs.tab_settings.field_registrar_lock'] = 'Registrar Lock'; $lang['Opensrs.tab_settings.field_registrar_lock_yes'] = 'Set the registrar lock. Recommended to prevent unauthorized transfer.'; $lang['Opensrs.tab_settings.field_registrar_lock_no'] = 'Release the registrar lock so the domain can be transferred.'; diff --git a/opensrs.php b/opensrs.php index 044d22c..6ff3a42 100644 --- a/opensrs.php +++ b/opensrs.php @@ -1122,12 +1122,23 @@ public function getAdminServiceTabs($service) { Loader::loadModels($this, ['Packages']); + $package = null; + if (isset($service->package) && $service->package) { + $package = $service->package; + } elseif (isset($service->pricing_id)) { + $package = $this->Packages->getByPricingId($service->pricing_id); + } + $tabs = [ 'tabWhois' => Language::_('Opensrs.tab_whois.title', true), 'tabNameservers' => Language::_('Opensrs.tab_nameservers.title', true), 'tabSettings' => Language::_('Opensrs.tab_settings.title', true) ]; + if ($this->featureServiceEnabled('dns_management', $service)) { + $tabs['tabDns'] = Language::_('Opensrs.tab_dns.title', true); + } + return $tabs; } @@ -1163,6 +1174,13 @@ public function getClientServiceTabs($service) ] ]; + if ($this->featureServiceEnabled('dns_management', $service)) { + $tabs['tabClientDns'] = [ + 'name' => Language::_('Opensrs.tab_dns.title', true), + 'icon' => 'fas fa-globe' + ]; + } + return $tabs; } @@ -1256,6 +1274,36 @@ public function tabClientSettings($package, $service, array $get = null, array $ return $this->manageSettings('tab_client_settings', $package, $service, $get, $post, $files); } + /** + * Admin DNS tab + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + public function tabDns($package, $service, array $get = null, array $post = null, array $files = null) + { + return $this->manageDns('tab_dns', $package, $service, $get, $post, $files); + } + + /** + * Client DNS tab + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + public function tabClientDns($package, $service, array $get = null, array $post = null, array $files = null) + { + return $this->manageDns('tab_client_dns', $package, $service, $get, $post, $files); + } + /** * Handle updating whois information * @@ -1459,6 +1507,144 @@ private function manageSettings( return $this->view->fetch(); } + /** + * Handle DNS zone management + * + * @param string $view The view to use + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + private function manageDns( + $view, + $package, + $service, + array $get = null, + array $post = null, + array $files = null + ) { + $this->view = new View($view, 'default'); + + // Load the helpers required for this view + Loader::loadHelpers($this, ['Form', 'Html']); + + $row = $this->getModuleRowOrFail($package->module_row); + if (!$row) { + return ''; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $vars = new stdClass(); + $fields = $this->serviceFieldsToObject($service->fields); + $dns = new OpensrsDomainsDns($api); + + if (!empty($post)) { + if (isset($post['action'])) { + if ($post['action'] == 'add_record') { + // Get existing records, add new one, set zone + $zone_response = $dns->getDnsZone(['domain' => $fields->domain]); + $this->processResponse($api, $zone_response); + + if ($zone_response->status() == 'OK') { + $zone = $zone_response->response(); + $records = $zone->attributes['records'] ?? []; + + // Add the new record + $new_record = [ + 'type' => $post['type'] ?? 'A', + 'subdomain' => $post['subdomain'] ?? '', + 'ip_address' => $post['ip_address'] ?? '', + 'priority' => $post['priority'] ?? '', + 'ttl' => $post['ttl'] ?? '3600' + ]; + + // Build the records for setDnsZone + $type_key = strtolower($new_record['type']); + if (!isset($records[$type_key]) || !is_array($records[$type_key])) { + $records[$type_key] = []; + } + $records[$type_key][] = $new_record; + + $response = $dns->setDnsZone([ + 'domain' => $fields->domain, + 'records' => $records + ]); + $this->processResponse($api, $response); + } + } elseif ($post['action'] == 'delete_record') { + // Get existing records, remove specified one, set zone + $zone_response = $dns->getDnsZone(['domain' => $fields->domain]); + $this->processResponse($api, $zone_response); + + if ($zone_response->status() == 'OK') { + $zone = $zone_response->response(); + $records = $zone->attributes['records'] ?? []; + + $delete_type = strtolower($post['record_type'] ?? ''); + $delete_index = (int)($post['record_index'] ?? -1); + + if (isset($records[$delete_type][$delete_index])) { + unset($records[$delete_type][$delete_index]); + $records[$delete_type] = array_values($records[$delete_type]); + } + + $response = $dns->setDnsZone([ + 'domain' => $fields->domain, + 'records' => $records + ]); + $this->processResponse($api, $response); + } + } elseif ($post['action'] == 'reset_zone') { + $response = $dns->resetDnsZone([ + 'domain' => $fields->domain + ]); + $this->processResponse($api, $response); + } + } + } + + // Fetch current zone records + $zone_response = $dns->getDnsZone(['domain' => $fields->domain]); + $records = []; + if ($zone_response->status() == 'OK') { + $zone = $zone_response->response(); + $raw_records = $zone->attributes['records'] ?? []; + + // Flatten records into a single array for display + foreach ($raw_records as $type => $type_records) { + if (is_array($type_records)) { + foreach ($type_records as $index => $record) { + if (is_array($record)) { + $record['record_type'] = strtoupper($type); + $record['record_index'] = $index; + $records[] = $record; + } + } + } + } + } else { + $this->Input->setErrors([]); + } + + $this->view->set('records', $records); + $this->view->set('vars', $vars); + $this->view->set('record_types', [ + 'A' => 'A', + 'AAAA' => 'AAAA', + 'CNAME' => 'CNAME', + 'MX' => 'MX', + 'TXT' => 'TXT', + 'SRV' => 'SRV', + 'NS' => 'NS' + ]); + $this->view->setDefaultView('components' . DS . 'modules' . DS . 'opensrs' . DS); + + return $this->view->fetch(); + } + /** * Verifies that the provided domain name is available * diff --git a/views/default/tab_client_dns.pdt b/views/default/tab_client_dns.pdt new file mode 100644 index 0000000..a9758c0 --- /dev/null +++ b/views/default/tab_client_dns.pdt @@ -0,0 +1,102 @@ + +
    +
    +

    _('Opensrs.tab_dns.title');?>

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    _('Opensrs.tab_dns.field_type');?>_('Opensrs.tab_dns.field_subdomain');?>_('Opensrs.tab_dns.field_ip_address');?>_('Opensrs.tab_dns.field_priority');?>_('Opensrs.tab_dns.field_ttl');?>
    Html->safe($record['record_type'] ?? '');?>Html->safe($record['subdomain'] ?? '');?>Html->safe($record['ip_address'] ?? '');?>Html->safe($record['priority'] ?? '');?>Html->safe($record['ttl'] ?? '');?> + Form->create(); + $this->Form->fieldHidden('action', 'delete_record'); + $this->Form->fieldHidden('record_type', strtolower($record['record_type'] ?? '')); + $this->Form->fieldHidden('record_index', $record['record_index'] ?? ''); + ?> + + Form->end(); + ?> +
    +
    + +
    _('Opensrs.tab_dns.no_records');?>
    + + +

    _('Opensrs.tab_dns.add_record');?>

    + Form->create(); + $this->Form->fieldHidden('action', 'add_record'); + ?> +
    + Form->label($this->_('Opensrs.tab_dns.field_type', true), 'type'); + $this->Form->fieldSelect('type', ($record_types ?? []), ($vars->type ?? 'A'), ['id' => 'type', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dns.field_subdomain', true), 'subdomain'); + $this->Form->fieldText('subdomain', ($vars->subdomain ?? null), ['id' => 'subdomain', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dns.field_ip_address', true), 'ip_address'); + $this->Form->fieldText('ip_address', ($vars->ip_address ?? null), ['id' => 'ip_address', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dns.field_priority', true), 'priority'); + $this->Form->fieldText('priority', ($vars->priority ?? null), ['id' => 'priority', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dns.field_ttl', true), 'ttl'); + $this->Form->fieldText('ttl', ($vars->ttl ?? '3600'), ['id' => 'ttl', 'class' => 'form-control']); + ?> +
    + + Form->end(); + ?> +
    +
    diff --git a/views/default/tab_dns.pdt b/views/default/tab_dns.pdt new file mode 100644 index 0000000..a42f18b --- /dev/null +++ b/views/default/tab_dns.pdt @@ -0,0 +1,117 @@ + +
    +

    _('Opensrs.tab_dns.title');?>

    +
    + + + + + + + + + + + + $record) { + ?> + > + + + + + + + + +
    _('Opensrs.tab_dns.field_type');?>_('Opensrs.tab_dns.field_subdomain');?>_('Opensrs.tab_dns.field_ip_address');?>_('Opensrs.tab_dns.field_priority');?>_('Opensrs.tab_dns.field_ttl');?>_('Opensrs.tab_dns.field_options');?>
    Html->safe($record['record_type'] ?? '');?>Html->safe($record['subdomain'] ?? '');?>Html->safe($record['ip_address'] ?? '');?>Html->safe($record['priority'] ?? '');?>Html->safe($record['ttl'] ?? '');?> + Form->create(); + $this->Form->fieldHidden('action', 'delete_record'); + $this->Form->fieldHidden('record_type', strtolower($record['record_type'] ?? '')); + $this->Form->fieldHidden('record_index', $record['record_index'] ?? ''); + ?> + + Form->end(); + ?> +
    + +
    +
    _('Opensrs.tab_dns.no_records');?>
    +
    + + +
    +

    _('Opensrs.tab_dns.add_record');?>

    +
    + Form->create(); + $this->Form->fieldHidden('action', 'add_record'); + ?> +
    +
      +
    • + Form->label($this->_('Opensrs.tab_dns.field_type', true), 'type'); + $this->Form->fieldSelect('type', ($record_types ?? []), ($vars->type ?? 'A'), ['id' => 'type']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dns.field_subdomain', true), 'subdomain'); + $this->Form->fieldText('subdomain', ($vars->subdomain ?? null), ['id' => 'subdomain']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dns.field_ip_address', true), 'ip_address'); + $this->Form->fieldText('ip_address', ($vars->ip_address ?? null), ['id' => 'ip_address']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dns.field_priority', true), 'priority'); + $this->Form->fieldText('priority', ($vars->priority ?? null), ['id' => 'priority']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dns.field_ttl', true), 'ttl'); + $this->Form->fieldText('ttl', ($vars->ttl ?? '3600'), ['id' => 'ttl']); + ?> +
    • +
    +
    +
    + Form->fieldSubmit('save', $this->_('Opensrs.tab_dns.field_add', true), ['class' => 'btn btn-primary float-right']); + ?> +
    + Form->end(); + ?> + +
    + Form->create(); + $this->Form->fieldHidden('action', 'reset_zone'); + ?> + + Form->end(); + ?> +
    From 693357edb659a3933afdbd523a819d4ed4d99b1a Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:38:24 +0000 Subject: [PATCH 09/13] Add URL forwarding API commands and management tabs - Create OpensrsDomainsForwarding class with get/set/create/delete methods for domain forwarding via the OpenSRS API - Register tabUrlForwarding/tabClientUrlForwarding in service tabs, conditional on dns_management feature - Add manageUrlForwarding() private method handling: - GET: Fetches forwarding records from DNS zone - POST set_forwarding: Adds forwarding rule with subdomain, destination URL, and redirect type (301/302/frame) - POST delete_forwarding: Removes forwarding rule by index - Create tab_url_forwarding.pdt (admin) and tab_client_url_forwarding.pdt (client) views - Add language keys for all forwarding labels --- apis/commands/opensrs_domains_forwarding.php | 80 ++++++++++ apis/opensrs_api.php | 2 + language/en_us/opensrs.php | 11 ++ opensrs.php | 153 +++++++++++++++++++ views/default/tab_client_url_forwarding.pdt | 85 +++++++++++ views/default/tab_url_forwarding.pdt | 87 +++++++++++ 6 files changed, 418 insertions(+) create mode 100644 apis/commands/opensrs_domains_forwarding.php create mode 100644 views/default/tab_client_url_forwarding.pdt create mode 100644 views/default/tab_url_forwarding.pdt diff --git a/apis/commands/opensrs_domains_forwarding.php b/apis/commands/opensrs_domains_forwarding.php new file mode 100644 index 0000000..d07efc7 --- /dev/null +++ b/apis/commands/opensrs_domains_forwarding.php @@ -0,0 +1,80 @@ +api = $api; + } + + /** + * Gets domain forwarding settings for the specified domain. + * + * @param array $vars An array of input params including: + * - domain The domain name + * @return OpensrsResponse The response object + */ + public function getDomainForwarding(array $vars) : OpensrsResponse + { + return $this->api->submit('get', array_merge($vars, ['type' => 'forwarding_email'])); + } + + /** + * Sets URL forwarding for the specified domain. + * + * @param array $vars An array of input params including: + * - domain The domain name + * - forwarding_uri The destination URL + * @return OpensrsResponse The response object + */ + public function setDomainForwarding(array $vars) : OpensrsResponse + { + return $this->api->submit('modify', array_merge($vars, [ + 'data' => 'forwarding_email', + 'affect_domains' => '0' + ])); + } + + /** + * Creates URL forwarding for the specified domain via DNS zone. + * + * @param array $vars An array of input params including: + * - source The source subdomain or @ for root + * - destination The destination URL + * - type The redirect type (301, 302, or frame) + * - domain The domain name + * @return OpensrsResponse The response object + */ + public function createDomainForwarding(array $vars) : OpensrsResponse + { + return $this->api->submit('set_dns_zone', $vars); + } + + /** + * Deletes URL forwarding for the specified domain. + * + * @param array $vars An array of input params including: + * - domain The domain name + * @return OpensrsResponse The response object + */ + public function deleteDomainForwarding(array $vars) : OpensrsResponse + { + return $this->api->submit('set_dns_zone', $vars); + } +} diff --git a/apis/opensrs_api.php b/apis/opensrs_api.php index 2b1d143..3f541cd 100644 --- a/apis/opensrs_api.php +++ b/apis/opensrs_api.php @@ -9,6 +9,8 @@ . DIRECTORY_SEPARATOR . 'opensrs_domains_provisioning.php'; require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR . 'opensrs_domains_transfer.php'; +require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' + . DIRECTORY_SEPARATOR . 'opensrs_domains_forwarding.php'; require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR . 'opensrs_ssl.php'; require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR . 'opensrs_users.php'; diff --git a/language/en_us/opensrs.php b/language/en_us/opensrs.php index 905eebe..1512c37 100755 --- a/language/en_us/opensrs.php +++ b/language/en_us/opensrs.php @@ -72,6 +72,17 @@ $lang['Opensrs.tab_dns.field_reset'] = 'Reset DNS Zone'; $lang['Opensrs.tab_dns.add_record'] = 'Add DNS Record'; $lang['Opensrs.tab_dns.no_records'] = 'There are no DNS records.'; + +$lang['Opensrs.tab_url_forwarding.title'] = 'URL Forwarding'; +$lang['Opensrs.tab_url_forwarding.field_subdomain'] = 'Subdomain'; +$lang['Opensrs.tab_url_forwarding.field_destination'] = 'Destination URL'; +$lang['Opensrs.tab_url_forwarding.field_redirect_type'] = 'Redirect Type'; +$lang['Opensrs.tab_url_forwarding.redirect_301'] = 'Permanent (301)'; +$lang['Opensrs.tab_url_forwarding.redirect_302'] = 'Temporary (302)'; +$lang['Opensrs.tab_url_forwarding.redirect_frame'] = 'Frame (Masked)'; +$lang['Opensrs.tab_url_forwarding.field_add'] = 'Add Forwarding Rule'; +$lang['Opensrs.tab_url_forwarding.add_record'] = 'Add URL Forwarding Rule'; +$lang['Opensrs.tab_url_forwarding.no_records'] = 'There are no URL forwarding rules.'; $lang['Opensrs.tab_settings.field_registrar_lock'] = 'Registrar Lock'; $lang['Opensrs.tab_settings.field_registrar_lock_yes'] = 'Set the registrar lock. Recommended to prevent unauthorized transfer.'; $lang['Opensrs.tab_settings.field_registrar_lock_no'] = 'Release the registrar lock so the domain can be transferred.'; diff --git a/opensrs.php b/opensrs.php index 6ff3a42..4a2845f 100644 --- a/opensrs.php +++ b/opensrs.php @@ -1137,6 +1137,7 @@ public function getAdminServiceTabs($service) if ($this->featureServiceEnabled('dns_management', $service)) { $tabs['tabDns'] = Language::_('Opensrs.tab_dns.title', true); + $tabs['tabUrlForwarding'] = Language::_('Opensrs.tab_url_forwarding.title', true); } return $tabs; @@ -1179,6 +1180,10 @@ public function getClientServiceTabs($service) 'name' => Language::_('Opensrs.tab_dns.title', true), 'icon' => 'fas fa-globe' ]; + $tabs['tabClientUrlForwarding'] = [ + 'name' => Language::_('Opensrs.tab_url_forwarding.title', true), + 'icon' => 'fas fa-share' + ]; } return $tabs; @@ -1304,6 +1309,41 @@ public function tabClientDns($package, $service, array $get = null, array $post return $this->manageDns('tab_client_dns', $package, $service, $get, $post, $files); } + /** + * Admin URL Forwarding tab + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + public function tabUrlForwarding($package, $service, array $get = null, array $post = null, array $files = null) + { + return $this->manageUrlForwarding('tab_url_forwarding', $package, $service, $get, $post, $files); + } + + /** + * Client URL Forwarding tab + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + public function tabClientUrlForwarding( + $package, + $service, + array $get = null, + array $post = null, + array $files = null + ) { + return $this->manageUrlForwarding('tab_client_url_forwarding', $package, $service, $get, $post, $files); + } + /** * Handle updating whois information * @@ -1645,6 +1685,119 @@ private function manageDns( return $this->view->fetch(); } + /** + * Handle URL forwarding management + * + * @param string $view The view to use + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + private function manageUrlForwarding( + $view, + $package, + $service, + array $get = null, + array $post = null, + array $files = null + ) { + $this->view = new View($view, 'default'); + + // Load the helpers required for this view + Loader::loadHelpers($this, ['Form', 'Html']); + + $row = $this->getModuleRowOrFail($package->module_row); + if (!$row) { + return ''; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $vars = new stdClass(); + $fields = $this->serviceFieldsToObject($service->fields); + $dns = new OpensrsDomainsDns($api); + + if (!empty($post)) { + if (isset($post['action'])) { + if ($post['action'] == 'set_forwarding') { + // Build URL forwarding record via DNS A/CNAME + URL forwarding + $zone_response = $dns->getDnsZone(['domain' => $fields->domain]); + $this->processResponse($api, $zone_response); + + if ($zone_response->status() == 'OK') { + $zone = $zone_response->response(); + $records = $zone->attributes['records'] ?? []; + + // Add forwarding record + if (!isset($records['url_forwarding']) || !is_array($records['url_forwarding'])) { + $records['url_forwarding'] = []; + } + $records['url_forwarding'][] = [ + 'subdomain' => $post['subdomain'] ?? '@', + 'ip_address' => $post['destination'] ?? '', + 'type' => $post['redirect_type'] ?? '301' + ]; + + $response = $dns->setDnsZone([ + 'domain' => $fields->domain, + 'records' => $records + ]); + $this->processResponse($api, $response); + } + } elseif ($post['action'] == 'delete_forwarding') { + $zone_response = $dns->getDnsZone(['domain' => $fields->domain]); + $this->processResponse($api, $zone_response); + + if ($zone_response->status() == 'OK') { + $zone = $zone_response->response(); + $records = $zone->attributes['records'] ?? []; + + $delete_index = (int)($post['record_index'] ?? -1); + if (isset($records['url_forwarding'][$delete_index])) { + unset($records['url_forwarding'][$delete_index]); + $records['url_forwarding'] = array_values($records['url_forwarding']); + } + + $response = $dns->setDnsZone([ + 'domain' => $fields->domain, + 'records' => $records + ]); + $this->processResponse($api, $response); + } + } + } + } + + // Fetch current forwarding records + $forwarding_records = []; + $zone_response = $dns->getDnsZone(['domain' => $fields->domain]); + if ($zone_response->status() == 'OK') { + $zone = $zone_response->response(); + $raw_records = $zone->attributes['records']['url_forwarding'] ?? []; + foreach ($raw_records as $index => $record) { + if (is_array($record)) { + $record['record_index'] = $index; + $forwarding_records[] = $record; + } + } + } else { + $this->Input->setErrors([]); + } + + $this->view->set('forwarding_records', $forwarding_records); + $this->view->set('vars', $vars); + $this->view->set('redirect_types', [ + '301' => Language::_('Opensrs.tab_url_forwarding.redirect_301', true), + '302' => Language::_('Opensrs.tab_url_forwarding.redirect_302', true), + 'frame' => Language::_('Opensrs.tab_url_forwarding.redirect_frame', true) + ]); + $this->view->setDefaultView('components' . DS . 'modules' . DS . 'opensrs' . DS); + + return $this->view->fetch(); + } + /** * Verifies that the provided domain name is available * diff --git a/views/default/tab_client_url_forwarding.pdt b/views/default/tab_client_url_forwarding.pdt new file mode 100644 index 0000000..782396a --- /dev/null +++ b/views/default/tab_client_url_forwarding.pdt @@ -0,0 +1,85 @@ + +
    +
    +

    _('Opensrs.tab_url_forwarding.title');?>

    + + +
    + + + + + + + + + + + + + + + + + + + +
    _('Opensrs.tab_url_forwarding.field_subdomain');?>_('Opensrs.tab_url_forwarding.field_destination');?>_('Opensrs.tab_url_forwarding.field_redirect_type');?>
    Html->safe($record['subdomain'] ?? '@');?>Html->safe($record['ip_address'] ?? '');?>Html->safe($record['type'] ?? '301');?> + Form->create(); + $this->Form->fieldHidden('action', 'delete_forwarding'); + $this->Form->fieldHidden('record_index', $record['record_index'] ?? ''); + ?> + + Form->end(); + ?> +
    +
    + +
    _('Opensrs.tab_url_forwarding.no_records');?>
    + + +

    _('Opensrs.tab_url_forwarding.add_record');?>

    + Form->create(); + $this->Form->fieldHidden('action', 'set_forwarding'); + ?> +
    + Form->label($this->_('Opensrs.tab_url_forwarding.field_subdomain', true), 'subdomain'); + $this->Form->fieldText('subdomain', ($vars->subdomain ?? '@'), ['id' => 'subdomain', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_url_forwarding.field_destination', true), 'destination'); + $this->Form->fieldText('destination', ($vars->destination ?? null), ['id' => 'destination', 'class' => 'form-control', 'placeholder' => 'https://example.com']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_url_forwarding.field_redirect_type', true), 'redirect_type'); + $this->Form->fieldSelect('redirect_type', ($redirect_types ?? []), ($vars->redirect_type ?? '301'), ['id' => 'redirect_type', 'class' => 'form-control']); + ?> +
    + + Form->end(); + ?> +
    +
    diff --git a/views/default/tab_url_forwarding.pdt b/views/default/tab_url_forwarding.pdt new file mode 100644 index 0000000..897c084 --- /dev/null +++ b/views/default/tab_url_forwarding.pdt @@ -0,0 +1,87 @@ + +
    +

    _('Opensrs.tab_url_forwarding.title');?>

    +
    + + + + + + + + + + $record) { + ?> + > + + + + + + +
    _('Opensrs.tab_url_forwarding.field_subdomain');?>_('Opensrs.tab_url_forwarding.field_destination');?>_('Opensrs.tab_url_forwarding.field_redirect_type');?>_('Opensrs.tab_dns.field_options');?>
    Html->safe($record['subdomain'] ?? '@');?>Html->safe($record['ip_address'] ?? '');?>Html->safe($record['type'] ?? '301');?> + Form->create(); + $this->Form->fieldHidden('action', 'delete_forwarding'); + $this->Form->fieldHidden('record_index', $record['record_index'] ?? ''); + ?> + + Form->end(); + ?> +
    + +
    +
    _('Opensrs.tab_url_forwarding.no_records');?>
    +
    + + +
    +

    _('Opensrs.tab_url_forwarding.add_record');?>

    +
    + Form->create(); + $this->Form->fieldHidden('action', 'set_forwarding'); + ?> +
    +
      +
    • + Form->label($this->_('Opensrs.tab_url_forwarding.field_subdomain', true), 'subdomain'); + $this->Form->fieldText('subdomain', ($vars->subdomain ?? '@'), ['id' => 'subdomain']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_url_forwarding.field_destination', true), 'destination'); + $this->Form->fieldText('destination', ($vars->destination ?? null), ['id' => 'destination', 'placeholder' => 'https://example.com']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_url_forwarding.field_redirect_type', true), 'redirect_type'); + $this->Form->fieldSelect('redirect_type', ($redirect_types ?? []), ($vars->redirect_type ?? '301'), ['id' => 'redirect_type']); + ?> +
    • +
    +
    +
    + Form->fieldSubmit('save', $this->_('Opensrs.tab_url_forwarding.field_add', true), ['class' => 'btn btn-primary float-right']); + ?> +
    + Form->end(); + ?> From 7fcb264250446eb39c6d974b4bd5e83e767f227f Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Thu, 26 Feb 2026 23:40:28 +0000 Subject: [PATCH 10/13] Add DNSSEC DS record management API and tabs - Create OpensrsDomainsDnssec class wrapping provisioning modify with data='dnssec' for add/remove, and get with type='domain_auth_info' for fetching existing DS records - Register tabDnssec/tabClientDnssec in service tabs, conditional on dns_management feature - Add manageDnssec() private method handling: - GET: Fetches existing DS records - POST add_ds_record: Adds DS record with key_tag, algorithm, digest_type, and digest fields - POST delete_ds_record: Removes DS record by index - Create tab_dnssec.pdt (admin) and tab_client_dnssec.pdt (client) views with DS record table and add form - Provide algorithm (3-16) and digest type (1,2,4) dropdowns with human-readable labels - Add all DNSSEC-related language keys --- apis/commands/opensrs_domains_dnssec.php | 77 +++++++++++ apis/opensrs_api.php | 2 + language/en_us/opensrs.php | 9 ++ opensrs.php | 158 +++++++++++++++++++++++ views/default/tab_client_dnssec.pdt | 93 +++++++++++++ views/default/tab_dnssec.pdt | 95 ++++++++++++++ 6 files changed, 434 insertions(+) create mode 100644 apis/commands/opensrs_domains_dnssec.php create mode 100644 views/default/tab_client_dnssec.pdt create mode 100644 views/default/tab_dnssec.pdt diff --git a/apis/commands/opensrs_domains_dnssec.php b/apis/commands/opensrs_domains_dnssec.php new file mode 100644 index 0000000..9f3d2dd --- /dev/null +++ b/apis/commands/opensrs_domains_dnssec.php @@ -0,0 +1,77 @@ + 'dnssec'. + * + * @copyright Copyright (c) 2021, Phillips Data, Inc. + * @license http://opensource.org/licenses/mit-license.php MIT License + * @package opensrs.commands + */ +class OpensrsDomainsDnssec +{ + /** + * @var OpensrsApi + */ + private $api; + + /** + * Sets the API to use for communication + * + * @param OpensrsApi $api The API to use for communication + */ + public function __construct(OpensrsApi $api) + { + $this->api = $api; + } + + /** + * Gets the DNSSEC DS records for a domain. + * + * @param array $vars An array of input params including: + * - domain The domain name + * @return OpensrsResponse The response object + */ + public function getDnssecRecords(array $vars) : OpensrsResponse + { + return $this->api->submit('get', array_merge($vars, [ + 'type' => 'domain_auth_info' + ])); + } + + /** + * Adds a DNSSEC DS record to a domain. + * + * @param array $vars An array of input params including: + * - domain The domain name + * - dnssec Array of DS records, each containing: + * - key_tag The key tag + * - algorithm The algorithm number + * - digest_type The digest type + * - digest The digest value + * @return OpensrsResponse The response object + */ + public function addDnssecRecord(array $vars) : OpensrsResponse + { + return $this->api->submit('modify', array_merge($vars, [ + 'data' => 'dnssec', + 'affect_domains' => '0' + ])); + } + + /** + * Removes DNSSEC DS records from a domain. + * + * @param array $vars An array of input params including: + * - domain The domain name + * - dnssec Empty array to clear all DS records + * @return OpensrsResponse The response object + */ + public function removeDnssecRecord(array $vars) : OpensrsResponse + { + return $this->api->submit('modify', array_merge($vars, [ + 'data' => 'dnssec', + 'affect_domains' => '0' + ])); + } +} diff --git a/apis/opensrs_api.php b/apis/opensrs_api.php index 3f541cd..e41309e 100644 --- a/apis/opensrs_api.php +++ b/apis/opensrs_api.php @@ -11,6 +11,8 @@ . DIRECTORY_SEPARATOR . 'opensrs_domains_transfer.php'; require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR . 'opensrs_domains_forwarding.php'; +require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' + . DIRECTORY_SEPARATOR . 'opensrs_domains_dnssec.php'; require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR . 'opensrs_ssl.php'; require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR . 'opensrs_users.php'; diff --git a/language/en_us/opensrs.php b/language/en_us/opensrs.php index 1512c37..4b73ef0 100755 --- a/language/en_us/opensrs.php +++ b/language/en_us/opensrs.php @@ -83,6 +83,15 @@ $lang['Opensrs.tab_url_forwarding.field_add'] = 'Add Forwarding Rule'; $lang['Opensrs.tab_url_forwarding.add_record'] = 'Add URL Forwarding Rule'; $lang['Opensrs.tab_url_forwarding.no_records'] = 'There are no URL forwarding rules.'; + +$lang['Opensrs.tab_dnssec.title'] = 'DNSSEC'; +$lang['Opensrs.tab_dnssec.field_key_tag'] = 'Key Tag'; +$lang['Opensrs.tab_dnssec.field_algorithm'] = 'Algorithm'; +$lang['Opensrs.tab_dnssec.field_digest_type'] = 'Digest Type'; +$lang['Opensrs.tab_dnssec.field_digest'] = 'Digest'; +$lang['Opensrs.tab_dnssec.field_add'] = 'Add DS Record'; +$lang['Opensrs.tab_dnssec.add_record'] = 'Add DS Record'; +$lang['Opensrs.tab_dnssec.no_records'] = 'There are no DNSSEC DS records.'; $lang['Opensrs.tab_settings.field_registrar_lock'] = 'Registrar Lock'; $lang['Opensrs.tab_settings.field_registrar_lock_yes'] = 'Set the registrar lock. Recommended to prevent unauthorized transfer.'; $lang['Opensrs.tab_settings.field_registrar_lock_no'] = 'Release the registrar lock so the domain can be transferred.'; diff --git a/opensrs.php b/opensrs.php index 4a2845f..1aeec6e 100644 --- a/opensrs.php +++ b/opensrs.php @@ -1138,6 +1138,7 @@ public function getAdminServiceTabs($service) if ($this->featureServiceEnabled('dns_management', $service)) { $tabs['tabDns'] = Language::_('Opensrs.tab_dns.title', true); $tabs['tabUrlForwarding'] = Language::_('Opensrs.tab_url_forwarding.title', true); + $tabs['tabDnssec'] = Language::_('Opensrs.tab_dnssec.title', true); } return $tabs; @@ -1184,6 +1185,10 @@ public function getClientServiceTabs($service) 'name' => Language::_('Opensrs.tab_url_forwarding.title', true), 'icon' => 'fas fa-share' ]; + $tabs['tabClientDnssec'] = [ + 'name' => Language::_('Opensrs.tab_dnssec.title', true), + 'icon' => 'fas fa-shield-alt' + ]; } return $tabs; @@ -1344,6 +1349,36 @@ public function tabClientUrlForwarding( return $this->manageUrlForwarding('tab_client_url_forwarding', $package, $service, $get, $post, $files); } + /** + * Admin DNSSEC tab + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + public function tabDnssec($package, $service, array $get = null, array $post = null, array $files = null) + { + return $this->manageDnssec('tab_dnssec', $package, $service, $get, $post, $files); + } + + /** + * Client DNSSEC tab + * + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + public function tabClientDnssec($package, $service, array $get = null, array $post = null, array $files = null) + { + return $this->manageDnssec('tab_client_dnssec', $package, $service, $get, $post, $files); + } + /** * Handle updating whois information * @@ -1798,6 +1833,129 @@ private function manageUrlForwarding( return $this->view->fetch(); } + /** + * Handle DNSSEC management + * + * @param string $view The view to use + * @param stdClass $package A stdClass object representing the current package + * @param stdClass $service A stdClass object representing the current service + * @param array $get Any GET parameters + * @param array $post Any POST parameters + * @param array $files Any FILES parameters + * @return string The string representing the contents of this tab + */ + private function manageDnssec( + $view, + $package, + $service, + array $get = null, + array $post = null, + array $files = null + ) { + $this->view = new View($view, 'default'); + + // Load the helpers required for this view + Loader::loadHelpers($this, ['Form', 'Html']); + + $row = $this->getModuleRowOrFail($package->module_row); + if (!$row) { + return ''; + } + $api = $this->getApi($row->meta->user, $row->meta->key, $row->meta->sandbox == 'true'); + + $vars = new stdClass(); + $fields = $this->serviceFieldsToObject($service->fields); + $dnssec = new OpensrsDomainsDnssec($api); + + if (!empty($post)) { + if (isset($post['action'])) { + if ($post['action'] == 'add_ds_record') { + // Get existing records, add new one + $info_response = $dnssec->getDnssecRecords(['domain' => $fields->domain]); + $existing_records = []; + if ($info_response->status() == 'OK') { + $info = $info_response->response(); + $existing_records = $info->attributes['dnssec'] ?? []; + } + + $new_record = [ + 'key_tag' => $post['key_tag'] ?? '', + 'algorithm' => $post['algorithm'] ?? '', + 'digest_type' => $post['digest_type'] ?? '', + 'digest' => $post['digest'] ?? '' + ]; + + $existing_records[] = $new_record; + + $response = $dnssec->addDnssecRecord([ + 'domain' => $fields->domain, + 'dnssec' => $existing_records + ]); + $this->processResponse($api, $response); + } elseif ($post['action'] == 'delete_ds_record') { + // Get existing records, remove specified one + $info_response = $dnssec->getDnssecRecords(['domain' => $fields->domain]); + $existing_records = []; + if ($info_response->status() == 'OK') { + $info = $info_response->response(); + $existing_records = $info->attributes['dnssec'] ?? []; + } + + $delete_index = (int)($post['record_index'] ?? -1); + if (isset($existing_records[$delete_index])) { + unset($existing_records[$delete_index]); + $existing_records = array_values($existing_records); + } + + $response = $dnssec->addDnssecRecord([ + 'domain' => $fields->domain, + 'dnssec' => $existing_records + ]); + $this->processResponse($api, $response); + } + } + } + + // Fetch current DS records + $ds_records = []; + $info_response = $dnssec->getDnssecRecords(['domain' => $fields->domain]); + if ($info_response->status() == 'OK') { + $info = $info_response->response(); + $raw_records = $info->attributes['dnssec'] ?? []; + foreach ($raw_records as $index => $record) { + if (is_array($record)) { + $record['record_index'] = $index; + $ds_records[] = $record; + } + } + } else { + $this->Input->setErrors([]); + } + + $this->view->set('ds_records', $ds_records); + $this->view->set('vars', $vars); + $this->view->set('algorithms', [ + '3' => '3 - DSA/SHA-1', + '5' => '5 - RSA/SHA-1', + '6' => '6 - DSA-NSEC3-SHA1', + '7' => '7 - RSASHA1-NSEC3-SHA1', + '8' => '8 - RSA/SHA-256', + '10' => '10 - RSA/SHA-512', + '13' => '13 - ECDSA/SHA-256', + '14' => '14 - ECDSA/SHA-384', + '15' => '15 - Ed25519', + '16' => '16 - Ed448' + ]); + $this->view->set('digest_types', [ + '1' => '1 - SHA-1', + '2' => '2 - SHA-256', + '4' => '4 - SHA-384' + ]); + $this->view->setDefaultView('components' . DS . 'modules' . DS . 'opensrs' . DS); + + return $this->view->fetch(); + } + /** * Verifies that the provided domain name is available * diff --git a/views/default/tab_client_dnssec.pdt b/views/default/tab_client_dnssec.pdt new file mode 100644 index 0000000..c3497d8 --- /dev/null +++ b/views/default/tab_client_dnssec.pdt @@ -0,0 +1,93 @@ + +
    +
    +

    _('Opensrs.tab_dnssec.title');?>

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    _('Opensrs.tab_dnssec.field_key_tag');?>_('Opensrs.tab_dnssec.field_algorithm');?>_('Opensrs.tab_dnssec.field_digest_type');?>_('Opensrs.tab_dnssec.field_digest');?>
    Html->safe($record['key_tag'] ?? '');?>Html->safe($record['algorithm'] ?? '');?>Html->safe($record['digest_type'] ?? '');?>Html->safe(substr($record['digest'] ?? '', 0, 32) . (strlen($record['digest'] ?? '') > 32 ? '...' : ''));?> + Form->create(); + $this->Form->fieldHidden('action', 'delete_ds_record'); + $this->Form->fieldHidden('record_index', $record['record_index'] ?? ''); + ?> + + Form->end(); + ?> +
    +
    + +
    _('Opensrs.tab_dnssec.no_records');?>
    + + +

    _('Opensrs.tab_dnssec.add_record');?>

    + Form->create(); + $this->Form->fieldHidden('action', 'add_ds_record'); + ?> +
    + Form->label($this->_('Opensrs.tab_dnssec.field_key_tag', true), 'key_tag'); + $this->Form->fieldText('key_tag', ($vars->key_tag ?? null), ['id' => 'key_tag', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dnssec.field_algorithm', true), 'algorithm'); + $this->Form->fieldSelect('algorithm', ($algorithms ?? []), ($vars->algorithm ?? '13'), ['id' => 'algorithm', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dnssec.field_digest_type', true), 'digest_type'); + $this->Form->fieldSelect('digest_type', ($digest_types ?? []), ($vars->digest_type ?? '2'), ['id' => 'digest_type', 'class' => 'form-control']); + ?> +
    +
    + Form->label($this->_('Opensrs.tab_dnssec.field_digest', true), 'digest'); + $this->Form->fieldText('digest', ($vars->digest ?? null), ['id' => 'digest', 'class' => 'form-control']); + ?> +
    + + Form->end(); + ?> +
    +
    diff --git a/views/default/tab_dnssec.pdt b/views/default/tab_dnssec.pdt new file mode 100644 index 0000000..2734aa6 --- /dev/null +++ b/views/default/tab_dnssec.pdt @@ -0,0 +1,95 @@ + +
    +

    _('Opensrs.tab_dnssec.title');?>

    +
    + + + + + + + + + + + $record) { + ?> + > + + + + + + + +
    _('Opensrs.tab_dnssec.field_key_tag');?>_('Opensrs.tab_dnssec.field_algorithm');?>_('Opensrs.tab_dnssec.field_digest_type');?>_('Opensrs.tab_dnssec.field_digest');?>_('Opensrs.tab_dns.field_options');?>
    Html->safe($record['key_tag'] ?? '');?>Html->safe($record['algorithm'] ?? '');?>Html->safe($record['digest_type'] ?? '');?>Html->safe(substr($record['digest'] ?? '', 0, 32) . (strlen($record['digest'] ?? '') > 32 ? '...' : ''));?> + Form->create(); + $this->Form->fieldHidden('action', 'delete_ds_record'); + $this->Form->fieldHidden('record_index', $record['record_index'] ?? ''); + ?> + + Form->end(); + ?> +
    + +
    +
    _('Opensrs.tab_dnssec.no_records');?>
    +
    + + +
    +

    _('Opensrs.tab_dnssec.add_record');?>

    +
    + Form->create(); + $this->Form->fieldHidden('action', 'add_ds_record'); + ?> +
    +
      +
    • + Form->label($this->_('Opensrs.tab_dnssec.field_key_tag', true), 'key_tag'); + $this->Form->fieldText('key_tag', ($vars->key_tag ?? null), ['id' => 'key_tag']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dnssec.field_algorithm', true), 'algorithm'); + $this->Form->fieldSelect('algorithm', ($algorithms ?? []), ($vars->algorithm ?? '13'), ['id' => 'algorithm']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dnssec.field_digest_type', true), 'digest_type'); + $this->Form->fieldSelect('digest_type', ($digest_types ?? []), ($vars->digest_type ?? '2'), ['id' => 'digest_type']); + ?> +
    • +
    • + Form->label($this->_('Opensrs.tab_dnssec.field_digest', true), 'digest'); + $this->Form->fieldText('digest', ($vars->digest ?? null), ['id' => 'digest']); + ?> +
    • +
    +
    +
    + Form->fieldSubmit('save', $this->_('Opensrs.tab_dnssec.field_add', true), ['class' => 'btn btn-primary float-right']); + ?> +
    + Form->end(); + ?> From a8d5787e9a3271eccc7c6901cd376e7ea58bc14b Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Fri, 27 Feb 2026 00:49:23 +0000 Subject: [PATCH 11/13] Fix DNSSEC get endpoint, postal code corruption, deprecations and error handling - Use type=dnssec for getDnssecRecords (was incorrectly using domain_auth_info) - Log all DNSSEC API fetches via logRequest for easier debugging - Remove erroneous Input::setErrors([]) that silently cleared preceding errors - Fix postal_code being overwritten with 00000 in setDomainContacts - Fix restoreDomain signature to match RegistrarModule parent (add array $vars = []) - Declare $logger property on OpensrsApi to fix dynamic property deprecation - Fix implicit nullable parameter deprecations across all tab and manage methods - Extract per-domain error detail from attributes.details in error responses - Move DNSSEC tabs outside dns_management feature flag (show unconditionally) --- apis/commands/opensrs_domains_dnssec.php | 2 +- apis/opensrs_api.php | 5 ++ apis/opensrs_response.php | 14 ++++ opensrs.php | 93 ++++++++++++------------ 4 files changed, 68 insertions(+), 46 deletions(-) diff --git a/apis/commands/opensrs_domains_dnssec.php b/apis/commands/opensrs_domains_dnssec.php index 9f3d2dd..ef1beb7 100644 --- a/apis/commands/opensrs_domains_dnssec.php +++ b/apis/commands/opensrs_domains_dnssec.php @@ -35,7 +35,7 @@ public function __construct(OpensrsApi $api) public function getDnssecRecords(array $vars) : OpensrsResponse { return $this->api->submit('get', array_merge($vars, [ - 'type' => 'domain_auth_info' + 'type' => 'dnssec' ])); } diff --git a/apis/opensrs_api.php b/apis/opensrs_api.php index e41309e..bc84356 100644 --- a/apis/opensrs_api.php +++ b/apis/opensrs_api.php @@ -48,6 +48,11 @@ class OpensrsApi */ private $sandbox; + /** + * @var mixed The logger instance + */ + private $logger; + /** * @var array An array representing the last request made */ diff --git a/apis/opensrs_response.php b/apis/opensrs_response.php index e43e2b4..9b89d24 100755 --- a/apis/opensrs_response.php +++ b/apis/opensrs_response.php @@ -76,6 +76,20 @@ public function errors() : ?object ?? $data->response_text ?? 'Internal Server Error'; + // Extract detailed per-domain error messages from attributes.details + $details = []; + if (isset($data->attributes['details']) && is_array($data->attributes['details'])) { + foreach ($data->attributes['details'] as $domain => $info) { + if (is_array($info) && !empty($info['response_text'])) { + $details[] = trim($info['response_text']); + } + } + } + + if (!empty($details)) { + $error_msg = implode('; ', $details); + } + return (object)[ 'response_text' => $error_msg, 'response_code' => $data->response_code ?? 500 diff --git a/opensrs.php b/opensrs.php index 1aeec6e..38897f0 100644 --- a/opensrs.php +++ b/opensrs.php @@ -211,7 +211,7 @@ public function getFilteredTldPricing($module_row_id = null, $filters = []) * @param array $vars An array of user supplied info to satisfy the request * @return bool True if the service validates, false otherwise. Sets Input errors when false. */ - public function validateService($package, array $vars = null) + public function validateService($package, ?array $vars = null) { $this->Input->setRules($this->getServiceRules($vars)); @@ -225,7 +225,7 @@ public function validateService($package, array $vars = null) * @param array $vars An array of user-supplied info to satisfy the request * @return bool True if the service update validates or false otherwise. Sets Input errors when false. */ - public function validateServiceEdit($service, array $vars = null) + public function validateServiceEdit($service, ?array $vars = null) { $this->Input->setRules($this->getServiceRules($vars, true)); @@ -239,7 +239,7 @@ public function validateServiceEdit($service, array $vars = null) * @param bool $edit True to get the edit rules, false for the add rules * @return array Service rules */ - private function getServiceRules(array $vars = null, $edit = false) + private function getServiceRules(?array $vars = null, $edit = false) { // Validate .fr TLD rules $fr_fields = Configure::get('Opensrs.domain_fields.fr'); @@ -323,7 +323,7 @@ private function getServiceRules(array $vars = null, $edit = false) */ public function addService( $package, - array $vars = null, + ?array $vars = null, $parent_package = null, $parent_service = null, $status = 'pending' @@ -650,7 +650,7 @@ public function editService($package, $service, array $vars = [], $parent_packag * @param int $module_row_id The ID of the module row to fetch for the current module * @return bool True if the domain was successfully restored, false otherwise */ - public function restoreDomain($domain, $module_row_id = null) + public function restoreDomain($domain, $module_row_id = null, array $vars = []) { $row = $this->getModuleRowOrFail($module_row_id); if (!$row) { @@ -679,7 +679,7 @@ public function restoreDomain($domain, $module_row_id = null) * @see Module::getModule() * @see Module::getModuleRow() */ - public function addPackage(array $vars = null) + public function addPackage(?array $vars = null) { $meta = []; if (isset($vars['meta']) && is_array($vars['meta'])) { @@ -711,7 +711,7 @@ public function addPackage(array $vars = null) * @see Module::getModule() * @see Module::getModuleRow() */ - public function editPackage($package, array $vars = null) + public function editPackage($package, ?array $vars = null) { $meta = []; if (isset($vars['meta']) && is_array($vars['meta'])) { @@ -1138,9 +1138,10 @@ public function getAdminServiceTabs($service) if ($this->featureServiceEnabled('dns_management', $service)) { $tabs['tabDns'] = Language::_('Opensrs.tab_dns.title', true); $tabs['tabUrlForwarding'] = Language::_('Opensrs.tab_url_forwarding.title', true); - $tabs['tabDnssec'] = Language::_('Opensrs.tab_dnssec.title', true); } + $tabs['tabDnssec'] = Language::_('Opensrs.tab_dnssec.title', true); + return $tabs; } @@ -1185,12 +1186,13 @@ public function getClientServiceTabs($service) 'name' => Language::_('Opensrs.tab_url_forwarding.title', true), 'icon' => 'fas fa-share' ]; - $tabs['tabClientDnssec'] = [ - 'name' => Language::_('Opensrs.tab_dnssec.title', true), - 'icon' => 'fas fa-shield-alt' - ]; } + $tabs['tabClientDnssec'] = [ + 'name' => Language::_('Opensrs.tab_dnssec.title', true), + 'icon' => 'fas fa-shield-alt' + ]; + return $tabs; } @@ -1204,7 +1206,7 @@ public function getClientServiceTabs($service) * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabWhois($package, $service, array $get = null, array $post = null, array $files = null) + public function tabWhois($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageWhois('tab_whois', $package, $service, $get, $post, $files); } @@ -1219,7 +1221,7 @@ public function tabWhois($package, $service, array $get = null, array $post = nu * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabClientWhois($package, $service, array $get = null, array $post = null, array $files = null) + public function tabClientWhois($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageWhois('tab_client_whois', $package, $service, $get, $post, $files); } @@ -1234,7 +1236,7 @@ public function tabClientWhois($package, $service, array $get = null, array $pos * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabNameservers($package, $service, array $get = null, array $post = null, array $files = null) + public function tabNameservers($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageNameservers('tab_nameservers', $package, $service, $get, $post, $files); } @@ -1249,7 +1251,7 @@ public function tabNameservers($package, $service, array $get = null, array $pos * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabClientNameservers($package, $service, array $get = null, array $post = null, array $files = null) + public function tabClientNameservers($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageNameservers('tab_client_nameservers', $package, $service, $get, $post, $files); } @@ -1264,7 +1266,7 @@ public function tabClientNameservers($package, $service, array $get = null, arra * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabSettings($package, $service, array $get = null, array $post = null, array $files = null) + public function tabSettings($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageSettings('tab_settings', $package, $service, $get, $post, $files); } @@ -1279,7 +1281,7 @@ public function tabSettings($package, $service, array $get = null, array $post = * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabClientSettings($package, $service, array $get = null, array $post = null, array $files = null) + public function tabClientSettings($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageSettings('tab_client_settings', $package, $service, $get, $post, $files); } @@ -1294,7 +1296,7 @@ public function tabClientSettings($package, $service, array $get = null, array $ * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabDns($package, $service, array $get = null, array $post = null, array $files = null) + public function tabDns($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageDns('tab_dns', $package, $service, $get, $post, $files); } @@ -1309,7 +1311,7 @@ public function tabDns($package, $service, array $get = null, array $post = null * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabClientDns($package, $service, array $get = null, array $post = null, array $files = null) + public function tabClientDns($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageDns('tab_client_dns', $package, $service, $get, $post, $files); } @@ -1324,7 +1326,7 @@ public function tabClientDns($package, $service, array $get = null, array $post * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabUrlForwarding($package, $service, array $get = null, array $post = null, array $files = null) + public function tabUrlForwarding($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageUrlForwarding('tab_url_forwarding', $package, $service, $get, $post, $files); } @@ -1342,9 +1344,9 @@ public function tabUrlForwarding($package, $service, array $get = null, array $p public function tabClientUrlForwarding( $package, $service, - array $get = null, - array $post = null, - array $files = null + ?array $get = null, + ?array $post = null, + ?array $files = null ) { return $this->manageUrlForwarding('tab_client_url_forwarding', $package, $service, $get, $post, $files); } @@ -1359,7 +1361,7 @@ public function tabClientUrlForwarding( * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabDnssec($package, $service, array $get = null, array $post = null, array $files = null) + public function tabDnssec($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageDnssec('tab_dnssec', $package, $service, $get, $post, $files); } @@ -1374,7 +1376,7 @@ public function tabDnssec($package, $service, array $get = null, array $post = n * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - public function tabClientDnssec($package, $service, array $get = null, array $post = null, array $files = null) + public function tabClientDnssec($package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { return $this->manageDnssec('tab_client_dnssec', $package, $service, $get, $post, $files); } @@ -1390,7 +1392,7 @@ public function tabClientDnssec($package, $service, array $get = null, array $po * @param array $files Any FILES parameters * @return string The string representing the contents of this tab */ - private function manageWhois($view, $package, $service, array $get = null, array $post = null, array $files = null) + private function manageWhois($view, $package, $service, ?array $get = null, ?array $post = null, ?array $files = null) { $this->view = new View($view, 'default'); @@ -1465,9 +1467,9 @@ private function manageNameservers( $view, $package, $service, - array $get = null, - array $post = null, - array $files = null + ?array $get = null, + ?array $post = null, + ?array $files = null ) { $this->view = new View($view, 'default'); @@ -1515,9 +1517,9 @@ private function manageSettings( $view, $package, $service, - array $get = null, - array $post = null, - array $files = null + ?array $get = null, + ?array $post = null, + ?array $files = null ) { $this->view = new View($view, 'default'); @@ -1597,9 +1599,9 @@ private function manageDns( $view, $package, $service, - array $get = null, - array $post = null, - array $files = null + ?array $get = null, + ?array $post = null, + ?array $files = null ) { $this->view = new View($view, 'default'); @@ -1735,9 +1737,9 @@ private function manageUrlForwarding( $view, $package, $service, - array $get = null, - array $post = null, - array $files = null + ?array $get = null, + ?array $post = null, + ?array $files = null ) { $this->view = new View($view, 'default'); @@ -1848,9 +1850,9 @@ private function manageDnssec( $view, $package, $service, - array $get = null, - array $post = null, - array $files = null + ?array $get = null, + ?array $post = null, + ?array $files = null ) { $this->view = new View($view, 'default'); @@ -1872,6 +1874,7 @@ private function manageDnssec( if ($post['action'] == 'add_ds_record') { // Get existing records, add new one $info_response = $dnssec->getDnssecRecords(['domain' => $fields->domain]); + $this->logRequest($api, $info_response); $existing_records = []; if ($info_response->status() == 'OK') { $info = $info_response->response(); @@ -1895,6 +1898,7 @@ private function manageDnssec( } elseif ($post['action'] == 'delete_ds_record') { // Get existing records, remove specified one $info_response = $dnssec->getDnssecRecords(['domain' => $fields->domain]); + $this->logRequest($api, $info_response); $existing_records = []; if ($info_response->status() == 'OK') { $info = $info_response->response(); @@ -1919,6 +1923,7 @@ private function manageDnssec( // Fetch current DS records $ds_records = []; $info_response = $dnssec->getDnssecRecords(['domain' => $fields->domain]); + $this->logRequest($api, $info_response); if ($info_response->status() == 'OK') { $info = $info_response->response(); $raw_records = $info->attributes['dnssec'] ?? []; @@ -1928,8 +1933,6 @@ private function manageDnssec( $ds_records[] = $record; } } - } else { - $this->Input->setErrors([]); } $this->view->set('ds_records', $ds_records); @@ -2098,7 +2101,7 @@ public function setDomainContacts($domain, array $vars = [], $module_row_id = nu $contact_set = []; foreach ($vars as $contact) { $contact['phone'] = $this->formatPhone($contact['phone'], $contact['country']); - $contact['postal_code'] = $contact['zip'] ?? '00000'; + $contact['postal_code'] = $contact['postal_code'] ?? $contact['zip'] ?? '00000'; $contact_set[$contact['external_id'] ?? 'owner'] = $contact; } From 5f1d649c980aa73aa401aa114d7eae4045eb2296 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Fri, 27 Feb 2026 06:57:09 +0000 Subject: [PATCH 12/13] Add Opensrs.countries lookup table and convert country fields to dropdowns Defines Opensrs.countries as a full ISO 3166-1 alpha-2 country code map at the top of the config file. Converts all free-text country inputs (owner, tech, admin, billing whois contacts and .law/.abogado jurisdiction country) to select fields backed by this lookup table. Co-Authored-By: Claude Sonnet 4.6 --- config/opensrs.php | 269 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 6 deletions(-) diff --git a/config/opensrs.php b/config/opensrs.php index 6b293ec..8153c1a 100755 --- a/config/opensrs.php +++ b/config/opensrs.php @@ -1,4 +1,256 @@ "Afghanistan", +"AX" => "Åland Islands", +"AL" => "Albania", +"DZ" => "Algeria", +"AS" => "American Samoa", +"AD" => "Andorra", +"AO" => "Angola", +"AI" => "Anguilla", +"AQ" => "Antarctica", +"AG" => "Antigua and Barbuda", +"AR" => "Argentina", +"AM" => "Armenia", +"AW" => "Aruba", +"AU" => "Australia", +"AT" => "Austria", +"AZ" => "Azerbaijan", +"BS" => "Bahamas", +"BH" => "Bahrain", +"BD" => "Bangladesh", +"BB" => "Barbados", +"BY" => "Belarus", +"BE" => "Belgium", +"BZ" => "Belize", +"BJ" => "Benin", +"BM" => "Bermuda", +"BT" => "Bhutan", +"BO" => "Bolivia, Plurinational State of", +"BQ" => "Bonaire, Sint Eustatius and Saba", +"BA" => "Bosnia and Herzegovina", +"BW" => "Botswana", +"BV" => "Bouvet Island", +"BR" => "Brazil", +"IO" => "British Indian Ocean Territory", +"BN" => "Brunei Darussalam", +"BG" => "Bulgaria", +"BF" => "Burkina Faso", +"BI" => "Burundi", +"CV" => "Cabo Verde", +"KH" => "Cambodia", +"CM" => "Cameroon", +"CA" => "Canada", +"KY" => "Cayman Islands", +"CF" => "Central African Republic", +"TD" => "Chad", +"CL" => "Chile", +"CN" => "China", +"CX" => "Christmas Island", +"CC" => "Cocos (Keeling) Islands", +"CO" => "Colombia", +"KM" => "Comoros", +"CG" => "Congo", +"CD" => "Congo, Democratic Republic of the", +"CK" => "Cook Islands", +"CR" => "Costa Rica", +"CI" => "Côte d'Ivoire", +"HR" => "Croatia", +"CU" => "Cuba", +"CW" => "Curaçao", +"CY" => "Cyprus", +"CZ" => "Czechia", +"DK" => "Denmark", +"DJ" => "Djibouti", +"DM" => "Dominica", +"DO" => "Dominican Republic", +"EC" => "Ecuador", +"EG" => "Egypt", +"SV" => "El Salvador", +"GQ" => "Equatorial Guinea", +"ER" => "Eritrea", +"EE" => "Estonia", +"SZ" => "Eswatini", +"ET" => "Ethiopia", +"FK" => "Falkland Islands (Malvinas)", +"FO" => "Faroe Islands", +"FJ" => "Fiji", +"FI" => "Finland", +"FR" => "France", +"GF" => "French Guiana", +"PF" => "French Polynesia", +"TF" => "French Southern Territories", +"GA" => "Gabon", +"GM" => "Gambia", +"GE" => "Georgia", +"DE" => "Germany", +"GH" => "Ghana", +"GI" => "Gibraltar", +"GR" => "Greece", +"GL" => "Greenland", +"GD" => "Grenada", +"GP" => "Guadeloupe", +"GU" => "Guam", +"GT" => "Guatemala", +"GG" => "Guernsey", +"GN" => "Guinea", +"GW" => "Guinea-Bissau", +"GY" => "Guyana", +"HT" => "Haiti", +"HM" => "Heard Island and McDonald Islands", +"VA" => "Holy See", +"HN" => "Honduras", +"HK" => "Hong Kong", +"HU" => "Hungary", +"IS" => "Iceland", +"IN" => "India", +"ID" => "Indonesia", +"IR" => "Iran, Islamic Republic of", +"IQ" => "Iraq", +"IE" => "Ireland", +"IM" => "Isle of Man", +"IL" => "Israel", +"IT" => "Italy", +"JM" => "Jamaica", +"JP" => "Japan", +"JE" => "Jersey", +"JO" => "Jordan", +"KZ" => "Kazakhstan", +"KE" => "Kenya", +"KI" => "Kiribati", +"KP" => "Korea, Democratic People's Republic of", +"KR" => "Korea, Republic of", +"KW" => "Kuwait", +"KG" => "Kyrgyzstan", +"LA" => "Lao People's Democratic Republic", +"LV" => "Latvia", +"LB" => "Lebanon", +"LS" => "Lesotho", +"LR" => "Liberia", +"LY" => "Libya", +"LI" => "Liechtenstein", +"LT" => "Lithuania", +"LU" => "Luxembourg", +"MO" => "Macao", +"MG" => "Madagascar", +"MW" => "Malawi", +"MY" => "Malaysia", +"MV" => "Maldives", +"ML" => "Mali", +"MT" => "Malta", +"MH" => "Marshall Islands", +"MQ" => "Martinique", +"MR" => "Mauritania", +"MU" => "Mauritius", +"YT" => "Mayotte", +"MX" => "Mexico", +"FM" => "Micronesia, Federated States of", +"MD" => "Moldova, Republic of", +"MC" => "Monaco", +"MN" => "Mongolia", +"ME" => "Montenegro", +"MS" => "Montserrat", +"MA" => "Morocco", +"MZ" => "Mozambique", +"MM" => "Myanmar", +"NA" => "Namibia", +"NR" => "Nauru", +"NP" => "Nepal", +"NL" => "Netherlands, Kingdom of the", +"NC" => "New Caledonia", +"NZ" => "New Zealand", +"NI" => "Nicaragua", +"NE" => "Niger", +"NG" => "Nigeria", +"NU" => "Niue", +"NF" => "Norfolk Island", +"MK" => "North Macedonia", +"MP" => "Northern Mariana Islands", +"NO" => "Norway", +"OM" => "Oman", +"PK" => "Pakistan", +"PW" => "Palau", +"PS" => "Palestine, State of", +"PA" => "Panama", +"PG" => "Papua New Guinea", +"PY" => "Paraguay", +"PE" => "Peru", +"PH" => "Philippines", +"PN" => "Pitcairn", +"PL" => "Poland", +"PT" => "Portugal", +"PR" => "Puerto Rico", +"QA" => "Qatar", +"RE" => "Réunion", +"RO" => "Romania", +"RU" => "Russian Federation", +"RW" => "Rwanda", +"BL" => "Saint Barthélemy", +"SH" => "Saint Helena, Ascension and Tristan da Cunha", +"KN" => "Saint Kitts and Nevis", +"LC" => "Saint Lucia", +"MF" => "Saint Martin (French part)", +"PM" => "Saint Pierre and Miquelon", +"VC" => "Saint Vincent and the Grenadines", +"WS" => "Samoa", +"SM" => "San Marino", +"ST" => "Sao Tome and Principe", +"SA" => "Saudi Arabia", +"SN" => "Senegal", +"RS" => "Serbia", +"SC" => "Seychelles", +"SL" => "Sierra Leone", +"SG" => "Singapore", +"SX" => "Sint Maarten (Dutch part)", +"SK" => "Slovakia", +"SI" => "Slovenia", +"SB" => "Solomon Islands", +"SO" => "Somalia", +"ZA" => "South Africa", +"GS" => "South Georgia and the South Sandwich Islands", +"SS" => "South Sudan", +"ES" => "Spain", +"LK" => "Sri Lanka", +"SD" => "Sudan", +"SR" => "Suriname", +"SJ" => "Svalbard and Jan Mayen", +"SE" => "Sweden", +"CH" => "Switzerland", +"SY" => "Syrian Arab Republic", +"TW" => "Taiwan", +"TJ" => "Tajikistan", +"TZ" => "Tanzania, United Republic of", +"TH" => "Thailand", +"TL" => "Timor-Leste", +"TG" => "Togo", +"TK" => "Tokelau", +"TO" => "Tonga", +"TT" => "Trinidad and Tobago", +"TN" => "Tunisia", +"TR" => "Türkiye", +"TM" => "Turkmenistan", +"TC" => "Turks and Caicos Islands", +"TV" => "Tuvalu", +"UG" => "Uganda", +"UA" => "Ukraine", +"AE" => "United Arab Emirates", +"GB" => "United Kingdom of Great Britain and Northern Ireland", +"US" => "United States of America", +"UM" => "United States Minor Outlying Islands", +"UY" => "Uruguay", +"UZ" => "Uzbekistan", +"VU" => "Vanuatu", +"VE" => "Venezuela", +"VN" => "Viet Nam", +"VG" => "Virgin Islands (British)", +"VI" => "Virgin Islands (U.S.)", +"WF" => "Wallis and Futuna", +"EH" => "Western Sahara", +"YE" => "Yemen", +"ZM" => "Zambia", +"ZW" => "Zimbabwe", +]); // All available TLDs Configure::set('Opensrs.tlds', [ '.abogado', @@ -844,7 +1096,8 @@ ], 'contact_set[owner][country]' => [ 'label' => Language::_('Opensrs.whois.owner.country', true), - 'type' => 'text' + 'type' => 'select', + 'options' => Configure::get('Opensrs.countries') ], 'contact_set[owner][phone]' => [ 'label' => Language::_('Opensrs.whois.owner.phone', true), @@ -888,7 +1141,8 @@ ], 'contact_set[tech][country]' => [ 'label' => Language::_('Opensrs.whois.tech.country', true), - 'type' => 'text' + 'type' => 'select', + 'options' => Configure::get('Opensrs.countries') ], 'contact_set[tech][phone]' => [ 'label' => Language::_('Opensrs.whois.tech.phone', true), @@ -932,7 +1186,8 @@ ], 'contact_set[admin][country]' => [ 'label' => Language::_('Opensrs.whois.admin.country', true), - 'type' => 'text' + 'type' => 'select', + 'options' => Configure::get('Opensrs.countries') ], 'contact_set[admin][phone]' => [ 'label' => Language::_('Opensrs.whois.admin.phone', true), @@ -976,7 +1231,8 @@ ], 'contact_set[billing][country]' => [ 'label' => Language::_('Opensrs.whois.billing.country', true), - 'type' => 'text' + 'type' => 'select', + 'options' => Configure::get('Opensrs.countries') ], 'contact_set[billing][phone]' => [ 'label' => Language::_('Opensrs.whois.billing.phone', true), @@ -1190,7 +1446,8 @@ ], 'tld_data[registrant_extra_info][qli_jurisdiction_country]' => [ 'label' => Language::_('Opensrs.domain.qli_jurisdiction_country', true), - 'type' => 'text' + 'type' => 'select', + 'options' => Configure::get('Opensrs.countries') ], 'tld_data[registrant_extra_info][qli_jurisdiction_state]' => [ 'label' => Language::_('Opensrs.domain.qli_jurisdiction_state', true), @@ -1224,4 +1481,4 @@

    Domain: {service.domain}

    Thank you for your business!

    ' ] -]); \ No newline at end of file +]); From ed2dfa86c83b55876cff91b9672216f4a9f1b140 Mon Sep 17 00:00:00 2001 From: Jon Morby Date: Fri, 6 Mar 2026 10:45:01 +0000 Subject: [PATCH 13/13] Fix empty array serialized as dt_assoc instead of dt_array When deleting the last DNSSEC record, the remaining empty array was serialized as because isset([][0]) is false. OpenSRS requires for the dnssec attribute, causing "Array/list expected for dnssec attribute" errors on delete. Treat empty arrays as lists so they serialize as dt_array, consistent with non-empty sequential arrays. Co-Authored-By: Claude Sonnet 4.6 --- apis/opensrs_api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/opensrs_api.php b/apis/opensrs_api.php index bc84356..d536b26 100644 --- a/apis/opensrs_api.php +++ b/apis/opensrs_api.php @@ -196,7 +196,7 @@ private function buildRecursiveAttributes(SimpleXMLElement &$dt_assoc, array $ar if (is_array($value)) { $assoc = $dt_assoc->addChild('item'); $assoc->addAttribute('key', $key); - $assoc = $assoc->addChild(isset($value[0]) ? 'dt_array' : 'dt_assoc'); + $assoc = $assoc->addChild((empty($value) || isset($value[0])) ? 'dt_array' : 'dt_assoc'); $this->buildRecursiveAttributes($assoc, $value); } else {