From f19972ddb340f71d3ffbead57c396b8e289dc9b6 Mon Sep 17 00:00:00 2001 From: Stormvdm Date: Tue, 6 Oct 2026 14:29:29 +0200 Subject: [PATCH] Add WHM findDnsZone so one DNS cluster lookup can see an existing zone. parse_dns_zone replaces deprecated dumpzone and avoids querying every web server. Co-authored-by: Cursor --- README.md | 5 ++- src/Contracts/WhmInterface.php | 10 +++++ src/Whm.php | 38 +++++++++++++++++ tests/Feature/WhmTest.php | 47 ++++++++++++++++++++- tests/stubs/whm/parse_dns_zone_missing.json | 8 ++++ tests/stubs/whm/parse_dns_zone_success.json | 24 +++++++++++ 6 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/stubs/whm/parse_dns_zone_missing.json create mode 100644 tests/stubs/whm/parse_dns_zone_success.json diff --git a/README.md b/README.md index 42c62bc..98b4494 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ Whm::parkDomain('alias.example.com', 'cpanel_username', 'example.com'); $owner = Whm::domainOwner('alias.example.com'); // cPanel username or null $parked = Whm::listParkedDomains('cpanel_username'); $match = Whm::findParkedDomain('alias.example.com'); // parked domain row or null +$zone = Whm::findDnsZone('alias.example.com'); // DNS zone on this cluster member, or null Cpanel::createEmail('user@example.com', 'password'); Cpanel::deleteEmail('user@example.com'); @@ -234,14 +235,16 @@ Create a token in WHM → **Development → Manage API Tokens**. Whitelist the c | Park (alias) a domain | `Whm::parkDomain()` → `create_parked_domain_for_user` | `park-dns` | DNS → Park DNS Zones | | List parked domains for an account | `Whm::listParkedDomains()` → cPanel API2 `Park::listparkeddomains` via `/json-api/cpanel` | `cpanel-api` | Initial Privileges → Perform cPanel API and UAPI functions through the WHM API | | Find one parked domain (owner lookup + list) | `Whm::findParkedDomain()` | `list-accts` **and** `cpanel-api` | As above | +| Look up one DNS zone on the cluster | `Whm::findDnsZone()` → `parse_dns_zone` | `list-zones` | DNS → List Zones | -**Minimum set for parking + package listing:** `park-dns`, `list-accts`, `cpanel-api`, `list-pkgs`. +**Minimum set for parking + package listing:** `park-dns`, `list-accts`, `cpanel-api`, `list-pkgs`. Add `list-zones` when you also call `findDnsZone()`. Notes: - `listParkedDomains` / email helpers go through the WHM cPanel proxy, so they need `cpanel-api`, not a separate “list parked domains” ACL. - `listPackages(want: 'all')` returns root and reseller packages; package create/edit/delete privileges are not required. +- `findDnsZone()` calls `parse_dns_zone` once against the configured WHM server. Use a DNS cluster member: a synchronized cluster already has every member's zones, so there is no need to query each web server. `dumpzone` is the deprecated equivalent. The call needs the DNS role; WHM disables it when that role is off. ## Testing diff --git a/src/Contracts/WhmInterface.php b/src/Contracts/WhmInterface.php index 0f7ed39..9ed3153 100644 --- a/src/Contracts/WhmInterface.php +++ b/src/Contracts/WhmInterface.php @@ -61,5 +61,15 @@ public function listParkedDomains(string $cpanelUsername, ?string $regex = null) */ public function findParkedDomain(string $domain, ?string $cpanelUsername = null): ?array; + /** + * Look up one DNS zone on this server. + * + * Point the client at a DNS cluster member. A synchronized cluster already + * holds every member's zones, so this is a single parse_dns_zone call. + * + * @return array{domain: string, payload: list}|null + */ + public function findDnsZone(string $domain): ?array; + public static function generatePassword(): string; } diff --git a/src/Whm.php b/src/Whm.php index be4ee5c..b7069ce 100644 --- a/src/Whm.php +++ b/src/Whm.php @@ -502,6 +502,44 @@ public function findParkedDomain(string $domain, ?string $cpanelUsername = null) return null; } + /** + * Look up one DNS zone on this server. + * + * WHM API 1 parse_dns_zone is the supported replacement for dumpzone. + * It asks the DNS role for a single zone. On a synchronized DNS cluster + * that role already has every member's zones, so callers should point + * this client at a cluster DNS server instead of querying each web server. + * Returns null when the zone is not present. + * + * @link https://api.docs.cpanel.net/specifications/whm.openapi/dns-zones/parse_dns_zone.md + * + * @return array{domain: string, payload: list}|null + */ + public function findDnsZone(string $domain): ?array + { + $domain = strtolower(rtrim(trim($domain), '.')); + + if ($domain === '') { + return null; + } + + $response = $this->client()->get('/json-api/parse_dns_zone', [ + 'api.version' => 1, + 'zone' => $domain, + ])->json(); + + if ((int) ($response['metadata']['result'] ?? 0) !== 1) { + return null; + } + + $payload = $response['data']['payload'] ?? []; + + return [ + 'domain' => $domain, + 'payload' => is_array($payload) ? array_values($payload) : [], + ]; + } + /** * Generate a random password of 12 characters */ diff --git a/tests/Feature/WhmTest.php b/tests/Feature/WhmTest.php index 0e6e4ab..8d49e11 100644 --- a/tests/Feature/WhmTest.php +++ b/tests/Feature/WhmTest.php @@ -171,7 +171,7 @@ $whm = new Whm(null, null, null); $whm->bandwidth(); -})->throws(\RuntimeException::class, 'WHM is not configured'); +})->throws(RuntimeException::class, 'WHM is not configured'); test('it can delete an email account successfully', function () { $whm = mock(WhmInterface::class); @@ -346,6 +346,51 @@ ->and($result['status'])->toBe('not redirected'); }); +test('findDnsZone returns the parsed zone from the DNS cluster member', function () { + $stub = json_decode(file_get_contents(__DIR__.'/../stubs/whm/parse_dns_zone_success.json'), true); + + Http::fake([ + 'test.example.com:2087/json-api/parse_dns_zone*' => Http::response($stub, 200), + ]); + + $api = new Whm('root', 'token', 'https://test.example.com:2087'); + + $result = $api->findDnsZone('Example.com.'); + + expect($result)->toBeArray() + ->and($result['domain'])->toBe('example.com') + ->and($result['payload'])->toHaveCount(2) + ->and($result['payload'][1]['type'])->toBe('record'); + + Http::assertSent(function ($request) { + return str_contains($request->url(), '/json-api/parse_dns_zone') + && $request['api.version'] == 1 + && $request['zone'] === 'example.com'; + }); +}); + +test('findDnsZone returns null when the zone is not in the cluster', function () { + $stub = json_decode(file_get_contents(__DIR__.'/../stubs/whm/parse_dns_zone_missing.json'), true); + + Http::fake([ + 'test.example.com:2087/json-api/parse_dns_zone*' => Http::response($stub, 200), + ]); + + $api = new Whm('root', 'token', 'https://test.example.com:2087'); + + expect($api->findDnsZone('missing.example.com'))->toBeNull(); +}); + +test('findDnsZone returns null for a blank domain without calling WHM', function () { + Http::fake(); + + $api = new Whm('root', 'token', 'https://test.example.com:2087'); + + expect($api->findDnsZone(' '))->toBeNull(); + + Http::assertNothingSent(); +}); + test('findParkedDomain returns null when the domain is not parked', function () { Http::fake([ 'test.example.com:2087/json-api/getdomainowner*' => Http::response( diff --git a/tests/stubs/whm/parse_dns_zone_missing.json b/tests/stubs/whm/parse_dns_zone_missing.json new file mode 100644 index 0000000..d4663d7 --- /dev/null +++ b/tests/stubs/whm/parse_dns_zone_missing.json @@ -0,0 +1,8 @@ +{ + "metadata": { + "command": "parse_dns_zone", + "reason": "The system could not find the specified zone.", + "result": 0, + "version": 1 + } +} diff --git a/tests/stubs/whm/parse_dns_zone_success.json b/tests/stubs/whm/parse_dns_zone_success.json new file mode 100644 index 0000000..934c3f1 --- /dev/null +++ b/tests/stubs/whm/parse_dns_zone_success.json @@ -0,0 +1,24 @@ +{ + "metadata": { + "command": "parse_dns_zone", + "reason": "OK", + "result": 1, + "version": 1 + }, + "data": { + "payload": [ + { + "line_index": 0, + "type": "control", + "dname_b64": "ZXhhbXBsZS5jb20u", + "text_b64": "JFRUTCAxNDQwMA==" + }, + { + "line_index": 1, + "type": "record", + "dname_b64": "ZXhhbXBsZS5jb20u", + "text_b64": "ZXhhbXBsZS5jb20uIElOIEEgMTkyLjAuMi4x" + } + ] + } +}