Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions src/Contracts/WhmInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed>}|null
*/
public function findDnsZone(string $domain): ?array;

public static function generatePassword(): string;
}
38 changes: 38 additions & 0 deletions src/Whm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed>}|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
*/
Expand Down
47 changes: 46 additions & 1 deletion tests/Feature/WhmTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions tests/stubs/whm/parse_dns_zone_missing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"metadata": {
"command": "parse_dns_zone",
"reason": "The system could not find the specified zone.",
"result": 0,
"version": 1
}
}
24 changes: 24 additions & 0 deletions tests/stubs/whm/parse_dns_zone_success.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}