From 981327158654685f379847a31a68628653e57b6b Mon Sep 17 00:00:00 2001 From: CodelineNL Date: Wed, 8 Mar 2023 10:16:15 +0200 Subject: [PATCH 1/6] Add Namecheap implementation --- README.md | 1 + src/LaravelServiceProvider.php | 2 + src/Namecheap/Data/NamecheapConfiguration.php | 29 + src/Namecheap/Helper/NamecheapApi.php | 602 ++++++++++++++++++ src/Namecheap/Provider.php | 502 +++++++++++++++ 5 files changed, 1136 insertions(+) create mode 100644 src/Namecheap/Data/NamecheapConfiguration.php create mode 100644 src/Namecheap/Helper/NamecheapApi.php create mode 100644 src/Namecheap/Provider.php diff --git a/README.md b/README.md index eafec60..c5b6fb9 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The following providers are currently implemented: - [NIRA](https://nira.ng/become-a-registrar) - [Ricta](https://www.ricta.org.rw/become-a-registrar/) - [UGRegistry](https://registry.co.ug/docs/v2/) + - [Namecheap](https://www.namecheap.com/support/api/methods/) ## Functions diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index fa070b6..b08a991 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -7,6 +7,7 @@ use Upmind\ProvisionBase\Laravel\ProvisionServiceProvider; use Upmind\ProvisionProviders\DomainNames\Category as DomainNames; use Upmind\ProvisionProviders\DomainNames\Example\Provider as ExampleProvider; +use Upmind\ProvisionProviders\DomainNames\Namecheap\Provider as Namecheap; use Upmind\ProvisionProviders\DomainNames\Nominet\Provider as Nominet; use Upmind\ProvisionProviders\DomainNames\Hexonet\Provider as Hexonet; use Upmind\ProvisionProviders\DomainNames\Enom\Provider as Enom; @@ -48,5 +49,6 @@ public function boot() $this->bindProvider('domain-names', 'ricta', Ricta::class); $this->bindProvider('domain-names', 'ug-registry', UGRegistry::class); $this->bindProvider('domain-names', 'domain-name-api', DomainNameApi::class); + $this->bindProvider('domain-names', 'namecheap', Namecheap::class); } } diff --git a/src/Namecheap/Data/NamecheapConfiguration.php b/src/Namecheap/Data/NamecheapConfiguration.php new file mode 100644 index 0000000..63c865b --- /dev/null +++ b/src/Namecheap/Data/NamecheapConfiguration.php @@ -0,0 +1,29 @@ + ['required', 'string', 'min:3', 'max:20'], + 'api_token' => ['required', 'string', 'min:6', 'max:50'], + 'sandbox' => ['nullable', 'boolean'], + 'debug' => ['nullable', 'boolean'], + ]); + } +} diff --git a/src/Namecheap/Helper/NamecheapApi.php b/src/Namecheap/Helper/NamecheapApi.php new file mode 100644 index 0000000..a12c226 --- /dev/null +++ b/src/Namecheap/Helper/NamecheapApi.php @@ -0,0 +1,602 @@ +client = $client; + $this->configuration = $configuration; + } + + /** + * @param string $domainList + * + * @return array + */ + public function checkMultipleDomains(string $domainList): array + { + $params = [ + 'command' => 'namecheap.domains.check', + 'DomainList' => $domainList, + ]; + + $response = $this->makeRequest($params); + + $dacDomains = []; + + foreach ($response->children() as $domain) { + $domainName = (string) $domain->attributes()->Domain; + + $available = (string) $domain->attributes()->Available === "true"; + + $canTransfer = false; + + if (!$available) { + $canTransfer = !$this->getRegistrarLockStatus($domainName); + } + + $dacDomains[] = DacDomain::create([ + 'domain' => $domainName, + 'description' => sprintf( + 'Domain is %s to register', + $available ? 'available' : 'not available' + ), + 'tld' => Utils::getTld($domainName), + 'can_register' => $available, + 'can_transfer' => $canTransfer, + 'is_premium' => $domain->attributes()->IsPremiumName === "true", + ]); + } + + return $dacDomains; + } + + /** + * @param string $domainName + * @param int $years + * @param array $contacts + * @param string $nameServers + * + * @return void + */ + public function register(string $domainName, int $years, array $contacts, string $nameServers): void + { + $params = [ + 'command' => 'namecheap.domains.create', + 'DomainName' => $domainName, + 'Years' => $years, + 'Nameservers' => $nameServers, + ]; + + foreach ($contacts as $type => $contact) { + $contactParams = $this->setContactParams($contact, $type); + $params = array_merge($params, $contactParams); + } + + $this->makeRequest($params); + } + + /** + * @param string $domainName + * @param string $eppCode + * + * @return string + */ + public function initiateTransfer(string $domainName, string $eppCode): string + { + $params = [ + 'command' => 'namecheap.domains.transfer.create', + 'DomainName' => $domainName, + 'EPPCode' => 'base64:'.base64_encode($eppCode), + 'Years' => 1, + ]; + + $response = $this->makeRequest($params)->DomainTransferCreateResult; + + return (string) $response->attributes()->TransferID; + } + + /** + * @param string $domainName + * @param int $period + * + * @return void + */ + public function renew(string $domainName, int $period): void + { + $params = [ + 'command' => 'namecheap.domains.renew', + 'DomainName' => $domainName, + 'Years' => $period, + ]; + + $this->makeRequest($params); + } + + /** + * @param string $domainName + * + * @return array + */ + public function getDomainInfo(string $domainName): array + { + $params = [ + 'command' => 'namecheap.domains.getinfo', + 'DomainName' => $domainName, + ]; + + $response = $this->makeRequest($params)->DomainGetInfoResult; + $lock = $this->getRegistrarLockStatus($domainName); + $contacts = $this->getContacts($domainName); + + $status = (string) $response->attributes()->Status; + + return [ + 'id' => (string) $response->attributes()->ID, + 'domain' => (string) $response->attributes()->DomainName, + 'statuses' => [$status === "Ok" ? 'Active' : $status], + 'locked' => $lock, + 'registrant' => $this->parseContact($contacts->Registrant, self::CONTACT_TYPE_REGISTRANT), + 'billing' => $this->parseContact($contacts->AuxBilling, self::CONTACT_TYPE_BILLING), + 'tech' => $this->parseContact($contacts->Tech, self::CONTACT_TYPE_TECH), + 'admin' => $this->parseContact($contacts->Admin, self::CONTACT_TYPE_ADMIN), + 'ns' => NameserversResult::create($this->parseNameservers($response->DnsDetails)), + 'created_at' => Utils::formatDate((string) $response->DomainDetails->CreatedDate), + 'updated_at' => null, + 'expires_at' => Utils::formatDate((string) $response->DomainDetails->ExpiredDate), + ]; + } + + /** + * @param string $domainName + * @param ContactParams $contactParams + * + * @return array + */ + public function updateRegistrantContact(string $domainName, ContactParams $contactParams): array + { + $currentContacts = $this->getContacts($domainName); + + $registrantParams = $this->setContactParams($contactParams, self::CONTACT_TYPE_REGISTRANT); + $techParams = $this->setXMLContactParams($currentContacts->Tech, self::CONTACT_TYPE_TECH); + $adminParams = $this->setXMLContactParams($currentContacts->Admin, self::CONTACT_TYPE_ADMIN); + $auxBillingParams = $this->setXMLContactParams($currentContacts->AuxBilling, self::CONTACT_TYPE_BILLING); + + $params = [ + 'command' => 'namecheap.domains.setContacts', + 'DomainName' => $domainName, + ]; + + $params = array_merge($params, $registrantParams, $techParams, $adminParams, $auxBillingParams); + + $this->makeRequest($params); + + $result = [ + 'organisation' => $contactParams->organisation ?: '-', + 'name' => $contactParams->name, + 'address1' => $contactParams->address1, + 'city' => $contactParams->city, + 'state' => $contactParams->state ?: '-', + 'postcode' => $contactParams->postcode, + 'country_code' => Utils::normalizeCountryCode($contactParams->country_code), + 'email' => $contactParams->email, + 'phone' => $contactParams->phone, + ]; + + if (isset($contactParams->organisation)) { + $result['organisation'] = $contactParams->organisation; + } + + return $result; + } + + /** + * @param string $sld + * @param string $tld + * @param string $nameservers + * + * @return array + */ + public function updateNameservers(string $sld, string $tld, string $nameservers): array + { + $command = 'namecheap.domains.dns.setCustom'; + + if ($nameservers === "") { + $command = 'namecheap.domains.dns.setDefault'; + } + + $params = [ + 'command' => $command, + 'SLD' => $sld, + 'TLD' => $tld, + 'NameServers' => $nameservers, + ]; + + $this->makeRequest($params); + + $ns = $this->getDNSList($sld, $tld); + + return $this->parseNameservers($ns); + } + + /** + * Send request and return the response. + * + * @param string $domainName + * @param bool $lock + * + * @return void + */ + public function setRegistrarLock(string $domainName, bool $lock): void + { + $params = [ + 'command' => 'namecheap.domains.setRegistrarLock', + 'DomainName' => $domainName, + 'LockAction' => $lock ? "lock" : "unlock", + ]; + + $this->makeRequest($params); + } + + /** + * @param string $sld + * @param string $tld + * + * @return SimpleXMLElement + */ + private function getDNSList(string $sld, string $tld): SimpleXMLElement + { + $params = [ + 'command' => 'namecheap.domains.dns.getList', + 'SLD' => $sld, + 'TLD' => $tld, + ]; + + return $this->makeRequest($params)->DomainDNSGetListResult; + } + + /** + * @param string $domainName + * + * @return bool + */ + public function getRegistrarLockStatus(string $domainName): bool + { + $params = [ + 'command' => 'namecheap.domains.getRegistrarLock', + 'DomainName' => $domainName, + ]; + + $tld = Utils::getTld($domainName); + + if (in_array($tld, self::NO_LOCK_TLDS)) { + return false; + } + + $response = $this->makeRequest($params); + $lockStatus = (string) $response->DomainGetRegistrarLockResult->attributes()->RegistrarLockStatus; + + return $lockStatus === "true"; + } + + /** + * @param string $domainName + * + * @return SimpleXMLElement + */ + private function getContacts(string $domainName): SimpleXMLElement + { + $params = [ + 'command' => 'namecheap.domains.getContacts', + 'DomainName' => $domainName, + ]; + + return $this->makeRequest($params)->DomainContactsResult; + } + + /** + * @param string $domainName + * + * @return array|null + */ + public function getDomainTransferOrders(string $domainName): ?array + { + $params = [ + 'command' => 'namecheap.domains.transfer.getlist', + 'SearchTerm' => $domainName, + 'ListType' => 'INPROGRESS', + ]; + + $response = $this->makeRequest($params); + $orderCount = (int) $response->Paging->TotalItems; + + $orders = []; + + if ($orderCount > 0) { + foreach ($response->TransferGetListResult->children() as $order) { + $orders[] = [ + 'orderId' => (string) $order->attributes()->OrderID, + 'status' => (string) $order->attributes()->Status, + 'statusId' => (int) $order->attributes()->StatusID, + 'date' => Utils::formatDate((string) $order->attributes()->StatusDate), + ]; + } + + if (count($orders) > 0) { + return $orders; + } + } + + return null; + } + + /** + * Send request and return the response. + * + * @param array $params + * + * @return SimpleXMLElement + * + * @throws ProvisionFunctionError + */ + public function makeRequest(array $params): SimpleXMLElement + { + // Prepare command params + $params = array_merge([ + 'ApiUser' => $this->configuration->username, + 'ApiKey' => $this->configuration->api_token, + 'UserName' => $this->configuration->username, + 'ClientIp' => request()->server('SERVER_ADDR'), + ], $params); + + $response = $this->client->get('/xml.response', ['query' => $params]); + + $result = $response->getBody()->__toString(); + $response->getBody()->close(); + + if (empty($result)) { + throw new RuntimeException('Empty Namecheap api response'); + } + + return $this->parseResponseData($result); + } + + /** + * Parse and process the XML Response + * + * @param string $result + * + * @return SimpleXMLElement + * @throws ProvisionFunctionError + */ + private function parseResponseData(string $result): SimpleXMLElement + { + // Try to parse the response + $xml = simplexml_load_string($result, 'SimpleXMLElement', LIBXML_NOCDATA); + + if ($xml === false) { + throw ProvisionFunctionError::create('Unknown Provider API Error') + ->withData([ + 'response' => $result, + ]); + } + + // Check the XML for errors + if ($errors = $this->parseXmlError($xml->Errors)) { + throw ProvisionFunctionError::create($this->formatNamecheapErrorMessage($errors)) + ->withData([ + 'response' => $xml, + ]); + } + + if (empty($xml->CommandResponse)) { + throw ProvisionFunctionError::create('No CommandResponse found') + ->withData([ + 'response' => $result, + ]); + } + + return $xml->CommandResponse; + } + + /** + * @param array $xmlErrors + * + * @return string + */ + private function formatNamecheapErrorMessage(array $xmlErrors): string + { + $errors = []; + + foreach ($xmlErrors as $error) { + switch ($error->attributes()->Number) { + case 1011150: + $errors[] = 'Rejected request - please review whitelisted IPs'; + break; + default: + $errors[] = (string) $error; + break; + } + } + + return sprintf("Provider API Errors: %s", implode(', ', $errors)); + } + + private function parseXmlError(SimpleXMLElement $errors): array + { + $result = []; + + foreach ($errors->children() as $err) { + $result[] = $err; + } + + return $result; + } + + /** + * @param SimpleXMLElement $contact + * @param string $type + * + * @return ContactData + */ + private function parseContact(SimpleXMLElement $contact, string $type): ContactData + { + // Check if our contact type is valid + self::validateContactType($type); + + return ContactData::create([ + 'organisation' => (string) $contact->OrganizationName ?: '-', + 'name' => $contact->FirstName." ".$contact->LastName, + 'address1' => (string) $contact->Address1, + 'city' => (string) $contact->City, + 'state' => (string) $contact->StateProvince ?: '-', + 'postcode' => (string) $contact->PostalCode, + 'country_code' => (string) $contact->Country, + 'email' => (string) $contact->EmailAddress, + 'phone' => (string) $contact->Phone, + ]); + } + + /** + * @param string $type + * + * @throws InvalidArgumentException + */ + public static function validateContactType(string $type): void + { + if (!in_array(strtolower($type), self::ALLOWED_CONTACT_TYPES)) { + throw new InvalidArgumentException(sprintf('Invalid contact type %s used!', $type)); + } + } + + /** + * @param SimpleXMLElement $DnsDetails + * + * @return array + */ + private function parseNameservers(SimpleXMLElement $DnsDetails): array + { + $result = []; + $i = 1; + + foreach ($DnsDetails->children() as $ns) { + $result['ns'.$i] = ['host' => (string) $ns]; + $i++; + } + + return $result; + } + + /** + * @param string|null $name + * + * @return array + */ + private function getNameParts(?string $name): array + { + $nameParts = explode(" ", $name); + $firstName = array_shift($nameParts); + $lastName = implode(" ", $nameParts); + + return compact('firstName', 'lastName'); + } + + /** + * @param SimpleXMLElement $contact + * @param string $type + * + * @return array + */ + private function setXMLContactParams(SimpleXMLElement $contact, string $type): array + { + return [ + $type.'OrganizationName' => (string) $contact->OrganizationName ?: '-', + $type.'FirstName' => (string) $contact->FirstName, + $type.'LastName' => (string) $contact->LastName, + $type.'Address1' => (string) $contact->Address1, + $type.'City' => (string) $contact->City, + $type.'StateProvince' => (string) $contact->StateProvince ?: '-', + $type.'PostalCode' => (string) $contact->PostalCode, + $type.'Country' => Utils::normalizeCountryCode((string) $contact->Country), + $type.'EmailAddress' => (string) $contact->EmailAddress, + $type.'Phone' => (string) $contact->Phone, + ]; + } + + /** + * @param ContactParams $contactParams + * @param string $type + * + * @return array + */ + private function setContactParams(ContactParams $contactParams, string $type): array + { + $nameParts = $this->getNameParts($contactParams->name ?? $contactParams->organisation); + + return [ + $type.'OrganizationName' => $contactParams->organisation ?: '-', + $type.'FirstName' => $nameParts['firstName'], + $type.'LastName' => $nameParts['lastName'], + $type.'Address1' => $contactParams->address1, + $type.'City' => $contactParams->city, + $type.'StateProvince' => $contactParams->state ?: '-', + $type.'PostalCode' => $contactParams->postcode, + $type.'Country' => Utils::normalizeCountryCode($contactParams->country_code), + $type.'EmailAddress' => $contactParams->email, + $type.'Phone' => $contactParams->phone, + ]; + } +} diff --git a/src/Namecheap/Provider.php b/src/Namecheap/Provider.php new file mode 100644 index 0000000..90770bf --- /dev/null +++ b/src/Namecheap/Provider.php @@ -0,0 +1,502 @@ +configuration = $configuration; + } + + /** + * @return AboutData + */ + public static function aboutProvider(): AboutData + { + return AboutData::create() + ->setName('Namecheap') + ->setDescription('Registering, hosting, and managing Namecheap domains') + //TODO upload logo file + ->setLogoUrl('https://api.upmind.io/images/logos/provision/namecheap-logo.png'); + } + + /** + * @param PollParams $params + * + * @return PollResult + */ + public function poll(PollParams $params): PollResult + { + throw $this->errorResult('Operation not supported'); + } + + /** + * @param DacParams $params + * + * @return DacResult + */ + public function domainAvailabilityCheck(DacParams $params): DacResult + { + $sld = Utils::normalizeSld($params->sld); + $domains = array_map( + fn($tld) => $sld.".".Utils::normalizeTld($tld), + $params->tlds + ); + $domainList = rtrim(implode(",", $domains), ','); + + $dacDomains = $this->api()->checkMultipleDomains($domainList); + + return DacResult::create([ + 'domains' => $dacDomains, + ]); + } + + /** + * @param RegisterDomainParams $params + * + * @return DomainResult + */ + public function register(RegisterDomainParams $params): DomainResult + { + $sld = Utils::normalizeSld($params->sld); + $tld = Utils::normalizeTld($params->tld); + $domainName = Utils::getDomain($sld, $tld); + + $this->checkRegisterParams($params); + + $checkResult = $this->api()->checkMultipleDomains($domainName); + + if (count($checkResult) < 1) { + throw $this->errorResult('Empty domain availability check result'); + } + + if (!$checkResult[0]->can_register) { + throw $this->errorResult('This domain is not available to register'); + } + + $contacts = [ + NamecheapApi::CONTACT_TYPE_REGISTRANT => $params->registrant->register, + NamecheapApi::CONTACT_TYPE_ADMIN => $params->admin->register, + NamecheapApi::CONTACT_TYPE_TECH => $params->tech->register, + NamecheapApi::CONTACT_TYPE_BILLING => $params->billing->register, + ]; + + try { + $this->api()->register( + $domainName, + intval($params->renew_years), + $contacts, + $this->prepareNameservers($params, 'nameservers.ns'), + ); + + return $this->_getInfo($domainName, sprintf('Domain %s was registered successfully!', $domainName)); + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param RegisterDomainParams $params + * + * @return void + */ + private function checkRegisterParams(RegisterDomainParams $params): void + { + if (!Arr::has($params, 'registrant.register')) { + throw $this->errorResult('Registrant contact data is required!'); + } + + if (!Arr::has($params, 'tech.register')) { + throw $this->errorResult('Tech contact data is required!'); + } + + if (!Arr::has($params, 'admin.register')) { + throw $this->errorResult('Admin contact data is required!'); + } + + if (!Arr::has($params, 'billing.register')) { + throw $this->errorResult('Billing contact data is required!'); + } + } + + + /** + * @param ArrayAccess $params + * @param string $prefix + * @return string + */ + private function prepareNameservers(ArrayAccess $params, string $prefix): string + { + $nameServers = ""; + + $custom = 0; + $default = 0; + + for ($i = 1; $i <= self::MAX_CUSTOM_NAMESERVERS; $i++) { + if (Arr::has($params, $prefix.$i)) { + $host = Arr::get($params, $prefix.$i)->host; + if (!in_array($host, self::DEFAULT_NAMESERVERS)) { + $nameServers .= $host.","; + $custom++; + } else { + $default++; + } + } + } + + if ($custom != 0 && $default != 0) { + throw $this->errorResult( + "It's not possible to mix Namecheap's default nameservers with other ones", + $params + ); + } + + if ($custom + $default < self::MIN_CUSTOM_NAMESERVERS) { + throw $this->errorResult('Minimum two nameservers are required!', $params); + } + + return $nameServers; + } + + /** + * @param TransferParams $params + * + * @return DomainResult + */ + public function transfer(TransferParams $params): DomainResult + { + $sld = Utils::normalizeSld($params->sld); + $tld = Utils::normalizeTld($params->tld); + + if (!in_array($tld, NamecheapApi::TRANSFER_TLD)) { + throw $this->errorResult(sprintf("Transfer is not available for TLD %s", $tld), $params); + } + + $domainName = Utils::getDomain($sld, $tld); + + $eppCode = $params->epp_code ?: '0000'; + + try { + return $this->_getInfo($domainName, 'Domain active in registrar account'); + } catch (Throwable $e) { + // domain not active - continue below + } + + try { + $prevOrder = $this->api()->getDomainTransferOrders($domainName); + + if (is_null($prevOrder)) { + $transferId = $this->api()->initiateTransfer($domainName, $eppCode); + + return DomainResult::create([ + 'id' => $transferId, + 'domain' => $domainName, + 'ns' => [], + 'statuses' => [], + 'created_at' => null, + 'updated_at' => null, + 'expires_at' => null, + ])->setMessage(sprintf('Transfer for %s domain successfully created!', $domainName)); + } else { + throw $this->errorResult( + sprintf('Transfer order(s) for %s already exists!', $domainName), + $prevOrder, + $params + ); + } + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param RenewParams $params + * + * @return DomainResult + */ + public function renew(RenewParams $params): DomainResult + { + $domainName = Utils::getDomain( + Utils::normalizeSld($params->sld), + Utils::normalizeTld($params->tld), + ); + $period = intval($params->renew_years); + + try { + $this->api()->renew($domainName, $period); + return $this->_getInfo($domainName, sprintf('Renewal for %s domain was successful!', $domainName)); + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param DomainInfoParams $params + * + * @return DomainResult + */ + public function getInfo(DomainInfoParams $params): DomainResult + { + $domainName = Utils::getDomain( + Utils::normalizeSld($params->sld), + Utils::normalizeTld($params->tld) + ); + + try { + return $this->_getInfo($domainName, 'Domain data obtained'); + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param UpdateDomainContactParams $params + * + * @return ContactResult + */ + public function updateRegistrantContact(UpdateDomainContactParams $params): ContactResult + { + try { + $contact = $this->api() + ->updateRegistrantContact( + Utils::getDomain( + Utils::normalizeSld($params->sld), + Utils::normalizeTld($params->tld) + ), + $params->contact + ); + + return ContactResult::create($contact); + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param UpdateNameserversParams $params + * + * @return NameserversResult + */ + public function updateNameservers(UpdateNameserversParams $params): NameserversResult + { + $sld = Utils::normalizeSld($params->sld); + $tld = Utils::normalizeTld($params->tld); + + $domainName = Utils::getDomain($sld, $tld); + + $nameServers = $this->prepareNameservers($params, 'ns'); + + try { + $result = $this->api()->updateNameservers( + $sld, + $tld, + $nameServers, + ); + + return NameserversResult::create($result) + ->setMessage(sprintf('Name servers for %s domain were updated!', $domainName)); + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param LockParams $params + * + * @return DomainResult + */ + public function setLock(LockParams $params): DomainResult + { + $domainName = Utils::getDomain($params->sld, $params->tld); + $lock = !!$params->lock; + + try { + if (in_array($params->tld, NamecheapApi::NO_LOCK_TLDS)) { + throw $this->errorResult( + sprintf('Domain %s does not support to change Registrar Lock.', $domainName), + $params + ); + } + + $currentLockStatus = $this->api()->getRegistrarLockStatus($domainName); + if (!$lock && !$currentLockStatus) { + throw $this->errorResult(sprintf('Domain %s already unlocked', $domainName), $params); + } + + if ($lock && $currentLockStatus) { + throw $this->errorResult(sprintf('Domain %s already locked', $domainName), $params); + } + + $this->api()->setRegistrarLock($domainName, $lock); + + return $this->_getInfo($domainName, sprintf("Lock %s!", $lock ? 'enabled' : 'disabled')); + } catch (\Throwable $e) { + $this->handleException($e, $params); + } + } + + /** + * @param AutoRenewParams $params + * + * @return DomainResult + */ + public function setAutoRenew(AutoRenewParams $params): DomainResult + { + throw $this->errorResult('Operation not supported'); + } + + /** + * @param EppParams $params + * + * @return EppCodeResult + */ + public function getEppCode(EppParams $params): EppCodeResult + { + throw $this->errorResult('Operation not supported'); + } + + /** + * @param IpsTagParams $params + * + * @return ResultData + */ + public function updateIpsTag(IpsTagParams $params): ResultData + { + throw $this->errorResult('Operation not supported'); + } + + /** + * @return no-return + * @throws ProvisionFunctionError + */ + protected function handleException(Throwable $e, $params = null): void + { + if (!$e instanceof ProvisionFunctionError) { + $e = new ProvisionFunctionError('Unexpected Provider Error', $e->getCode(), $e); + } + + throw $e->withDebug([ + 'params' => $params, + ]); + } + + /** + * @param string $domainName + * @param string $message + * + * @return DomainResult + */ + private function _getInfo(string $domainName, string $message): DomainResult + { + $domainInfo = $this->api()->getDomainInfo($domainName); + + return DomainResult::create($domainInfo)->setMessage($message); + } + + /** + * @return NamecheapApi + */ + protected function api(): NamecheapApi + { + if (isset($this->api)) { + return $this->api; + } + + $client = new Client([ + 'base_uri' => $this->resolveAPIURL(), + 'headers' => [ + 'User-Agent' => 'Upmind/ProvisionProviders/DomainNames/Namecheap', + ], + 'connect_timeout' => 10, + 'timeout' => 60, + 'verify' => !$this->configuration->sandbox, + 'handler' => $this->getGuzzleHandlerStack(boolval($this->configuration->debug)), + ]); + + return $this->api = new NamecheapApi($client, $this->configuration); + } + + /** + * @return string + */ + private function resolveAPIURL(): string + { + return $this->configuration->sandbox + ? 'https://api.sandbox.namecheap.com' + : 'https://api.namecheap.com'; + } +} From 3280a4ec164dd8e0106c2445428e296d2a8c1adf Mon Sep 17 00:00:00 2001 From: Elena Filippova Date: Thu, 9 Mar 2023 10:34:56 +0200 Subject: [PATCH 2/6] Add min validation for renew_years --- src/Data/RenewParams.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/RenewParams.php b/src/Data/RenewParams.php index 0fbb990..ef105bb 100644 --- a/src/Data/RenewParams.php +++ b/src/Data/RenewParams.php @@ -21,7 +21,7 @@ public static function rules(): Rules return new Rules([ 'sld' => ['required', 'alpha-dash'], 'tld' => ['required', 'alpha-dash-dot'], - 'renew_years' => ['required', 'integer', 'max:10'], + 'renew_years' => ['required', 'integer', 'min:1', 'max:10'], ]); } } From 6cd41658fbec83806631b9f100d88d9d59de944c Mon Sep 17 00:00:00 2001 From: Elena Filippova Date: Tue, 28 Mar 2023 14:34:44 +0300 Subject: [PATCH 3/6] PR edits: - add a unified 'phone' formatting; - add 'firstName' duplication in case the 'lastName' isn't set; - remove transferable TLDs check; - fix error and log messages; - change 'can_transfer' field calculation logic; - change return value of transfer() func; - change setLock's return value to success in case an input domain name has already been locked. --- src/Namecheap/Helper/NamecheapApi.php | 52 ++++++--------------------- src/Namecheap/Provider.php | 20 +++-------- 2 files changed, 15 insertions(+), 57 deletions(-) diff --git a/src/Namecheap/Helper/NamecheapApi.php b/src/Namecheap/Helper/NamecheapApi.php index a12c226..6391e42 100644 --- a/src/Namecheap/Helper/NamecheapApi.php +++ b/src/Namecheap/Helper/NamecheapApi.php @@ -34,14 +34,6 @@ class NamecheapApi 'io', ]; - public const TRANSFER_TLD = [ - "biz", "ca", "cc", "co", - "co.uk", "com", "com.es", "com.pe", - "es", "in", "info", "me", - "me.uk", "mobi", "net", "net.pe", - "nom.es", "org", "org.es", "org.pe", - "org.uk", "pe", "tv", "us", - ]; /** * Contact Types @@ -82,12 +74,6 @@ public function checkMultipleDomains(string $domainList): array $available = (string) $domain->attributes()->Available === "true"; - $canTransfer = false; - - if (!$available) { - $canTransfer = !$this->getRegistrarLockStatus($domainName); - } - $dacDomains[] = DacDomain::create([ 'domain' => $domainName, 'description' => sprintf( @@ -96,7 +82,7 @@ public function checkMultipleDomains(string $domainList): array ), 'tld' => Utils::getTld($domainName), 'can_register' => $available, - 'can_transfer' => $canTransfer, + 'can_transfer' => !$available, 'is_premium' => $domain->attributes()->IsPremiumName === "true", ]); } @@ -202,11 +188,11 @@ public function getDomainInfo(string $domainName): array /** * @param string $domainName - * @param ContactParams $contactParams + * @param ContactData $contactParams * * @return array */ - public function updateRegistrantContact(string $domainName, ContactParams $contactParams): array + public function updateRegistrantContact(string $domainName, ContactParams $contactParams): ContactData { $currentContacts = $this->getContacts($domainName); @@ -224,23 +210,9 @@ public function updateRegistrantContact(string $domainName, ContactParams $conta $this->makeRequest($params); - $result = [ - 'organisation' => $contactParams->organisation ?: '-', - 'name' => $contactParams->name, - 'address1' => $contactParams->address1, - 'city' => $contactParams->city, - 'state' => $contactParams->state ?: '-', - 'postcode' => $contactParams->postcode, - 'country_code' => Utils::normalizeCountryCode($contactParams->country_code), - 'email' => $contactParams->email, - 'phone' => $contactParams->phone, - ]; + $registrant = $this->getContacts($domainName)->Registrant; - if (isset($contactParams->organisation)) { - $result['organisation'] = $contactParams->organisation; - } - - return $result; + return $this->parseContact($registrant, self::CONTACT_TYPE_REGISTRANT); } /** @@ -273,8 +245,6 @@ public function updateNameservers(string $sld, string $tld, string $nameservers) } /** - * Send request and return the response. - * * @param string $domainName * @param bool $lock * @@ -408,7 +378,7 @@ public function makeRequest(array $params): SimpleXMLElement $response->getBody()->close(); if (empty($result)) { - throw new RuntimeException('Empty Namecheap api response'); + throw new RuntimeException('Empty provider api response'); } return $this->parseResponseData($result); @@ -504,9 +474,9 @@ private function parseContact(SimpleXMLElement $contact, string $type): ContactD 'city' => (string) $contact->City, 'state' => (string) $contact->StateProvince ?: '-', 'postcode' => (string) $contact->PostalCode, - 'country_code' => (string) $contact->Country, + 'country_code' => Utils::normalizeCountryCode((string) $contact->Country), 'email' => (string) $contact->EmailAddress, - 'phone' => (string) $contact->Phone, + 'phone' => Utils::internationalPhoneToEpp((string) $contact->Phone), ]); } @@ -572,7 +542,7 @@ private function setXMLContactParams(SimpleXMLElement $contact, string $type): a $type.'PostalCode' => (string) $contact->PostalCode, $type.'Country' => Utils::normalizeCountryCode((string) $contact->Country), $type.'EmailAddress' => (string) $contact->EmailAddress, - $type.'Phone' => (string) $contact->Phone, + $type.'Phone' => Utils::internationalPhoneToEpp((string) $contact->Phone), ]; } @@ -589,14 +559,14 @@ private function setContactParams(ContactParams $contactParams, string $type): a return [ $type.'OrganizationName' => $contactParams->organisation ?: '-', $type.'FirstName' => $nameParts['firstName'], - $type.'LastName' => $nameParts['lastName'], + $type.'LastName' => $nameParts['lastName']?: $nameParts['firstName'], $type.'Address1' => $contactParams->address1, $type.'City' => $contactParams->city, $type.'StateProvince' => $contactParams->state ?: '-', $type.'PostalCode' => $contactParams->postcode, $type.'Country' => Utils::normalizeCountryCode($contactParams->country_code), $type.'EmailAddress' => $contactParams->email, - $type.'Phone' => $contactParams->phone, + $type.'Phone' => Utils::internationalPhoneToEpp($contactParams->phone), ]; } } diff --git a/src/Namecheap/Provider.php b/src/Namecheap/Provider.php index 90770bf..8806d60 100644 --- a/src/Namecheap/Provider.php +++ b/src/Namecheap/Provider.php @@ -220,7 +220,7 @@ private function prepareNameservers(ArrayAccess $params, string $prefix): string if ($custom != 0 && $default != 0) { throw $this->errorResult( - "It's not possible to mix Namecheap's default nameservers with other ones", + "It's not possible to mix provider default nameservers with other ones", $params ); } @@ -242,10 +242,6 @@ public function transfer(TransferParams $params): DomainResult $sld = Utils::normalizeSld($params->sld); $tld = Utils::normalizeTld($params->tld); - if (!in_array($tld, NamecheapApi::TRANSFER_TLD)) { - throw $this->errorResult(sprintf("Transfer is not available for TLD %s", $tld), $params); - } - $domainName = Utils::getDomain($sld, $tld); $eppCode = $params->epp_code ?: '0000'; @@ -262,15 +258,7 @@ public function transfer(TransferParams $params): DomainResult if (is_null($prevOrder)) { $transferId = $this->api()->initiateTransfer($domainName, $eppCode); - return DomainResult::create([ - 'id' => $transferId, - 'domain' => $domainName, - 'ns' => [], - 'statuses' => [], - 'created_at' => null, - 'updated_at' => null, - 'expires_at' => null, - ])->setMessage(sprintf('Transfer for %s domain successfully created!', $domainName)); + throw $this->errorResult(sprintf('Transfer for %s domain successfully created!', $domainName), ['transfer_id' => $transferId]); } else { throw $this->errorResult( sprintf('Transfer order(s) for %s already exists!', $domainName), @@ -394,11 +382,11 @@ public function setLock(LockParams $params): DomainResult $currentLockStatus = $this->api()->getRegistrarLockStatus($domainName); if (!$lock && !$currentLockStatus) { - throw $this->errorResult(sprintf('Domain %s already unlocked', $domainName), $params); + return $this->_getInfo($domainName, sprintf('Domain %s already unlocked', $domainName)); } if ($lock && $currentLockStatus) { - throw $this->errorResult(sprintf('Domain %s already locked', $domainName), $params); + return $this->_getInfo($domainName, sprintf('Domain %s already locked', $domainName)); } $this->api()->setRegistrarLock($domainName, $lock); From 4ccea6203a4a7f4ae8b2af795ab5542fb7f72426 Mon Sep 17 00:00:00 2001 From: Harry Lewis Date: Wed, 5 Apr 2023 19:09:35 +0100 Subject: [PATCH 4/6] Fix class import --- src/Namecheap/Helper/NamecheapApi.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Namecheap/Helper/NamecheapApi.php b/src/Namecheap/Helper/NamecheapApi.php index 6391e42..5e6e958 100644 --- a/src/Namecheap/Helper/NamecheapApi.php +++ b/src/Namecheap/Helper/NamecheapApi.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Http\Request; use InvalidArgumentException; +use RuntimeException; use SimpleXMLElement; use Upmind\ProvisionBase\Exception\ProvisionFunctionError; use Upmind\ProvisionProviders\DomainNames\Data\ContactData; From 7d90c264b433e024c5c91a5bc4a81787ccc8e3fa Mon Sep 17 00:00:00 2001 From: Harry Lewis Date: Wed, 5 Apr 2023 19:10:08 +0100 Subject: [PATCH 5/6] Require upmind/provision-provider-base ^3.7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 04f07cb..5260806 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } }, "require": { - "upmind/provision-provider-base": "^3.0", + "upmind/provision-provider-base": "^3.7", "metaregistrar/php-epp-client": "^1.0", "hexonet/php-sdk": "^5.0", "propaganistas/laravel-phone": "^4.2", From 113763139efbd9494e884db3620b73f5184693bc Mon Sep 17 00:00:00 2001 From: Harry Lewis Date: Wed, 5 Apr 2023 19:11:24 +0100 Subject: [PATCH 6/6] Update NamecheapApi take ClientIp from SystemInfo --- src/Namecheap/Helper/NamecheapApi.php | 11 +++++++---- src/Namecheap/Provider.php | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Namecheap/Helper/NamecheapApi.php b/src/Namecheap/Helper/NamecheapApi.php index 5e6e958..e3e9016 100644 --- a/src/Namecheap/Helper/NamecheapApi.php +++ b/src/Namecheap/Helper/NamecheapApi.php @@ -7,11 +7,12 @@ use Carbon\Carbon; use GuzzleHttp\Client; use Illuminate\Support\Facades\Log; -use Illuminate\Http\Request; +use Illuminate\Support\Arr; use InvalidArgumentException; use RuntimeException; use SimpleXMLElement; use Upmind\ProvisionBase\Exception\ProvisionFunctionError; +use Upmind\ProvisionBase\Provider\DataSet\SystemInfo; use Upmind\ProvisionProviders\DomainNames\Data\ContactData; use Upmind\ProvisionProviders\DomainNames\Data\ContactParams; use Upmind\ProvisionProviders\DomainNames\Data\DacDomain; @@ -35,7 +36,6 @@ class NamecheapApi 'io', ]; - /** * Contact Types */ @@ -48,10 +48,13 @@ class NamecheapApi protected NamecheapConfiguration $configuration; - public function __construct(Client $client, NamecheapConfiguration $configuration) + protected SystemInfo $systemInfo; + + public function __construct(Client $client, NamecheapConfiguration $configuration, SystemInfo $systemInfo) { $this->client = $client; $this->configuration = $configuration; + $this->systemInfo = $systemInfo; } /** @@ -370,7 +373,7 @@ public function makeRequest(array $params): SimpleXMLElement 'ApiUser' => $this->configuration->username, 'ApiKey' => $this->configuration->api_token, 'UserName' => $this->configuration->username, - 'ClientIp' => request()->server('SERVER_ADDR'), + 'ClientIp' => Arr::first($this->systemInfo->outgoing_ips), ], $params); $response = $this->client->get('/xml.response', ['query' => $params]); diff --git a/src/Namecheap/Provider.php b/src/Namecheap/Provider.php index 8806d60..c889761 100644 --- a/src/Namecheap/Provider.php +++ b/src/Namecheap/Provider.php @@ -475,7 +475,7 @@ protected function api(): NamecheapApi 'handler' => $this->getGuzzleHandlerStack(boolval($this->configuration->debug)), ]); - return $this->api = new NamecheapApi($client, $this->configuration); + return $this->api = new NamecheapApi($client, $this->configuration, $this->getSystemInfo()); } /**