Skip to content

Commit d3aaebc

Browse files
committed
feat(domain): Add dedicated transfer API and verification metadata
1 parent 756d312 commit d3aaebc

16 files changed

Lines changed: 403 additions & 60 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[![Packagist Version](https://img.shields.io/packagist/v/rnids/rsreg-epp-client?label=Release&style=flat-square)](https://packagist.org/packages/rnids/rsreg-epp-client)
77
![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/rnids/rsreg-epp-client/php?label=PHP&logo=php&logoColor=white&logoSize=auto&style=flat-square)
88
![Static Badge](https://img.shields.io/badge/RNIDS-RSreg-3858e9?style=flat-square)
9+
[![codecov](https://codecov.io/github/oblakhost/rnids-rsreg-client-php/graph/badge.svg?token=0UMFP9CL35)](https://codecov.io/github/oblakhost/rnids-rsreg-client-php)
910
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/oblakhost/rnids-rsreg-client-php/release.yml?label=Build&event=push&style=flat-square&logo=githubactions&logoColor=white&logoSize=auto)](https://github.com/oblakhost/rnids-rsreg-client-php/actions/workflows/release.yml)
1011

1112
</div>
@@ -56,7 +57,7 @@ $client->close();
5657
Common fluent entry points:
5758

5859
- Session: `$client->session()->hello()`, `login()`, `logout()`, `poll()`
59-
- Domain: `$client->domain()->check()`, `info()`, `register()`, `renew()`, `update()`, `delete()`, `transfer()`
60+
- Domain: `$client->domain()->check()`, `info()`, `register()`, `renew()`, `update()`, `delete()`, `transfer()`, `getCode()`, `getState()`
6061
- Contact: `$client->contact()->check()`, `create()`, `info()`, `update()`, `delete()`
6162
- Host: `$client->host()->check()`, `info()`, `create()`, `update()`, `delete()`
6263

bin/rsreg

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33

44
declare(strict_types=1);
55

6-
require_once __DIR__ . '/../vendor/autoload.php';
6+
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
7+
if (file_exists($file)) {
8+
require $file;
9+
break;
10+
}
11+
}
712

813
/**
914
* @return never

composer.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api-contact.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ Policy behavior:
3232

3333
- `id` is optional. If omitted/empty, library auto-generates a contact ID.
3434
- Contact IDs are normalized to the `OBL-` prefix before sending create commands.
35+
- `postalInfo.name` is required by default.
36+
- Exception: it may be empty when `extension.isLegalEntity = '1'`
37+
and `postalInfo.organization` is provided.
3538
- `extension.identDescription` is enforced to:
3639
`Object Creation provided by Oblak Solutions.`
3740

docs/api-domain.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,17 @@ At least one mutation key must be present: `add`, `remove`, `registrant`, or `au
158158

159159
Deletes a domain.
160160

161-
### `transfer(array $request): array`
161+
### `transfer(string $domain, string $transferCode): array`
162162

163-
Handles transfer lifecycle operations (`request|query|cancel|approve|reject`).
163+
Approves a domain transfer using the provided transfer code.
164164

165-
Request shape:
165+
### `getCode(string $domain): array`
166166

167-
```php
168-
array{
169-
operation?: mixed,
170-
name?: mixed,
171-
period?: mixed,
172-
periodUnit?: mixed,
173-
authInfo?: mixed
174-
}
175-
```
167+
Runs transfer operation `request` for the domain.
168+
169+
### `getState(string $domain): array`
170+
171+
Runs transfer operation `query` for the domain.
176172

177173
Response shape:
178174

src/Contact/ContactRequestFactory.php

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ public function checkFromArray(array $request): ContactCheckRequest
4747
*/
4848
public function createFromArray(array $request): ContactCreateRequest
4949
{
50+
$extension = $this->optionalExtension($request, true);
51+
5052
return new ContactCreateRequest(
5153
$this->contactIdPolicy->normalizeForCreate($request['id'] ?? null),
52-
$this->requirePostalInfo($request),
54+
$this->requirePostalInfoForCreate($request, $extension),
5355
$this->optionalNullableString($request, 'voice'),
5456
$this->optionalNullableString($request, 'fax'),
5557
$this->requireString(
@@ -59,7 +61,7 @@ public function createFromArray(array $request): ContactCreateRequest
5961
),
6062
$this->optionalNullableString($request, 'authInfo'),
6163
$this->optionalDisclose($request),
62-
$this->optionalExtension($request, true),
64+
$extension,
6365
);
6466
}
6567

@@ -152,6 +154,24 @@ private function requirePostalInfo(array $request): ContactPostalInfo
152154
return $this->parsePostalInfo($postalInfo);
153155
}
154156

157+
/**
158+
* @param array<string, mixed> $request
159+
*/
160+
private function requirePostalInfoForCreate(array $request, ?ContactExtension $extension): ContactPostalInfo
161+
{
162+
$postalInfo = $this->requirePostalInfo($request);
163+
164+
if ('' !== \trim($postalInfo->name)) {
165+
return $postalInfo;
166+
}
167+
168+
if ($this->isLegalEntityCreateWithoutNaturalPersonName($extension, $postalInfo)) {
169+
return $postalInfo;
170+
}
171+
172+
throw new \InvalidArgumentException('Contact postalInfo key "name" must be a non-empty string.');
173+
}
174+
155175
/**
156176
* @param array<string, mixed> $request
157177
*/
@@ -169,7 +189,13 @@ private function optionalPostalInfo(array $request): ?ContactPostalInfo
169189
);
170190
}
171191

172-
return $this->parsePostalInfo($postalInfo);
192+
$parsedPostalInfo = $this->parsePostalInfo($postalInfo);
193+
194+
if ('' === \trim($parsedPostalInfo->name)) {
195+
throw new \InvalidArgumentException('Contact postalInfo key "name" must be a non-empty string.');
196+
}
197+
198+
return $parsedPostalInfo;
173199
}
174200

175201
/**
@@ -199,16 +225,22 @@ private function parsePostalInfo(array $postalInfo): ContactPostalInfo
199225

200226
return new ContactPostalInfo(
201227
$type,
202-
$this->requireString(
203-
$postalInfo,
204-
'name',
205-
'Contact postalInfo key "%s" must be a non-empty string.',
206-
),
228+
$this->optionalStringAllowingEmpty($postalInfo, 'name'),
207229
$this->optionalNullableString($postalInfo, 'organization'),
208230
$this->parseAddress($address),
209231
);
210232
}
211233

234+
private function isLegalEntityCreateWithoutNaturalPersonName(
235+
?ContactExtension $extension,
236+
ContactPostalInfo $postalInfo,
237+
): bool {
238+
return null !== $extension
239+
&& '1' === $extension->isLegalEntity
240+
&& null !== $postalInfo->organization
241+
&& '' !== \trim($postalInfo->organization);
242+
}
243+
212244
/**
213245
* @param array<string, mixed> $address
214246
*/
@@ -360,6 +392,20 @@ private function optionalNullableString(array $request, string $key): ?string
360392
return $value;
361393
}
362394

395+
/**
396+
* @param array<string, mixed> $request
397+
*/
398+
private function optionalStringAllowingEmpty(array $request, string $key): string
399+
{
400+
if (!\array_key_exists($key, $request) || !\is_string($request[$key])) {
401+
throw new \InvalidArgumentException(
402+
\sprintf('Contact postalInfo key "%s" must be a string.', $key),
403+
);
404+
}
405+
406+
return $request[$key];
407+
}
408+
363409
private function normalizeString(mixed $value, string $errorPattern, int $index): string
364410
{
365411
if (!\is_string($value) || '' === \trim($value)) {

src/Domain/DomainResponseMapper.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public function mapCheckResponse(DomainCheckResponse $response): array
4545
* updateDate: \DateTimeImmutable|null,
4646
* expirationDate: \DateTimeImmutable|null,
4747
* whoisPrivacy: bool,
48+
* isDomainVerified: bool,
49+
* domainVerifiedOn: \DateTimeImmutable|null,
50+
* domainVerificationRequestExpiresOn: \DateTimeImmutable|null,
51+
* isWhoisPrivacyPaid: bool,
4852
* operationMode: string|null,
4953
* notifyAdmin: bool,
5054
* dnsSec: bool,
@@ -59,7 +63,11 @@ public function mapInfoResponse(DomainInfoResponse $response): array
5963
'createClientId' => $response->createClientId,
6064
'createDate' => $response->createDate,
6165
'dnsSec' => $response->dnsSec,
66+
'domainVerificationRequestExpiresOn' => $response->domainVerificationRequestExpiresOn,
67+
'domainVerifiedOn' => $response->domainVerifiedOn,
6268
'expirationDate' => $response->expirationDate,
69+
'isDomainVerified' => $response->isDomainVerified,
70+
'isWhoisPrivacyPaid' => $response->isWhoisPrivacyPaid,
6371
'name' => $response->name,
6472
'nameservers' => $this->mapNameservers($response->nameservers),
6573
'notifyAdmin' => $response->notifyAdmin,

src/Domain/DomainService.php

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ public function check(string|array $request): array
156156
* updateDate: \DateTimeImmutable|null,
157157
* expirationDate: \DateTimeImmutable|null,
158158
* whoisPrivacy: bool,
159+
* isDomainVerified: bool,
160+
* domainVerifiedOn: \DateTimeImmutable|null,
161+
* domainVerificationRequestExpiresOn: \DateTimeImmutable|null,
162+
* isWhoisPrivacyPaid: bool,
159163
* operationMode: string|null,
160164
* notifyAdmin: bool,
161165
* dnsSec: bool,
@@ -328,13 +332,35 @@ public function update(array $request): array
328332
}
329333

330334
/**
331-
* @param array{
332-
* operation?: mixed,
333-
* name?: mixed,
334-
* period?: mixed,
335-
* periodUnit?: mixed,
336-
* authInfo?: mixed
337-
* } $request
335+
* Approves a domain transfer using the provided transfer code.
336+
*
337+
* @return array{
338+
* name: string|null,
339+
* transferStatus: string|null,
340+
* requestClientId: string|null,
341+
* requestDate: \DateTimeImmutable|null,
342+
* actionClientId: string|null,
343+
* actionDate: \DateTimeImmutable|null,
344+
* expirationDate: \DateTimeImmutable|null
345+
* }
346+
*/
347+
public function transfer(string $domain, string $transferCode): array
348+
{
349+
$name = $this->inputNormalizer->requireDomainName($domain);
350+
351+
if ('' === \trim($transferCode)) {
352+
throw new \InvalidArgumentException('Domain transfer code must be a non-empty string.');
353+
}
354+
355+
return $this->executeTransferOperation(
356+
DomainTransferRequest::OPERATION_APPROVE,
357+
$name,
358+
$transferCode,
359+
);
360+
}
361+
362+
/**
363+
* Starts the transfer flow and requests transfer code-related state from the registry.
338364
*
339365
* @return array{
340366
* name: string|null,
@@ -346,15 +372,55 @@ public function update(array $request): array
346372
* expirationDate: \DateTimeImmutable|null
347373
* }
348374
*/
349-
public function transfer(array $request): array
375+
public function getCode(string $domain): array
376+
{
377+
return $this->executeTransferOperation(
378+
DomainTransferRequest::OPERATION_REQUEST,
379+
$this->inputNormalizer->requireDomainName($domain),
380+
);
381+
}
382+
383+
/**
384+
* Queries current transfer state for a domain.
385+
*
386+
* @return array{
387+
* name: string|null,
388+
* transferStatus: string|null,
389+
* requestClientId: string|null,
390+
* requestDate: \DateTimeImmutable|null,
391+
* actionClientId: string|null,
392+
* actionDate: \DateTimeImmutable|null,
393+
* expirationDate: \DateTimeImmutable|null
394+
* }
395+
*/
396+
public function getState(string $domain): array
397+
{
398+
return $this->executeTransferOperation(
399+
DomainTransferRequest::OPERATION_QUERY,
400+
$this->inputNormalizer->requireDomainName($domain),
401+
);
402+
}
403+
404+
/**
405+
* @return array{
406+
* name: string|null,
407+
* transferStatus: string|null,
408+
* requestClientId: string|null,
409+
* requestDate: \DateTimeImmutable|null,
410+
* actionClientId: string|null,
411+
* actionDate: \DateTimeImmutable|null,
412+
* expirationDate: \DateTimeImmutable|null
413+
* }
414+
*/
415+
private function executeTransferOperation(string $operation, string $name, ?string $authInfo = null): array
350416
{
351417
$xml = $this->transferRequestBuilder->build(
352418
new DomainTransferRequest(
353-
$this->inputNormalizer->requireTransferOperation($request),
354-
$this->inputNormalizer->requireName($request),
355-
$this->inputNormalizer->optionalPositiveInt($request, 'period'),
356-
$this->inputNormalizer->optionalPeriodUnit($request),
357-
$this->inputNormalizer->optionalNullableString($request, 'authInfo'),
419+
$operation,
420+
$name,
421+
null,
422+
'y',
423+
$authInfo,
358424
),
359425
$this->tridGenerator->nextId(),
360426
);

src/Domain/Dto/DomainInfoResponse.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public function __construct(
2828
public readonly ?\DateTimeImmutable $updateDate,
2929
public readonly ?\DateTimeImmutable $expirationDate,
3030
public readonly bool $whoisPrivacy,
31+
public readonly bool $isDomainVerified,
32+
public readonly ?\DateTimeImmutable $domainVerifiedOn,
33+
public readonly ?\DateTimeImmutable $domainVerificationRequestExpiresOn,
34+
public readonly bool $isWhoisPrivacyPaid,
3135
public readonly ?string $operationMode,
3236
public readonly bool $notifyAdmin,
3337
public readonly bool $dnsSec,

src/Xml/Contact/ContactInfoResponseParser.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,11 @@ private function parsePostalInfo(\DOMXPath $xpath): ?ContactPostalInfo
153153

154154
$type = $this->normalizePostalInfoType($postalInfoNode);
155155

156-
$name = $this->firstRelativeNodeValue($xpath, 'contact:name', $postalInfoNode);
156+
$name = $this->firstRelativeNodeValue($xpath, 'contact:name', $postalInfoNode) ?? '';
157157
$city = $this->firstRelativeNodeValue($xpath, 'contact:addr/contact:city', $postalInfoNode);
158158
$countryCode = $this->firstRelativeNodeValue($xpath, 'contact:addr/contact:cc', $postalInfoNode);
159159

160-
if (null === $name || null === $city || null === $countryCode) {
160+
if (null === $city || null === $countryCode) {
161161
return null;
162162
}
163163

0 commit comments

Comments
 (0)