diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index 3fb03b2..07403e4 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -16,6 +16,7 @@ use Upmind\ProvisionProviders\SharedHosting\InterWorx\Provider as InterWorx; use Upmind\ProvisionProviders\SharedHosting\DirectAdmin\Provider as DirectAdmin; use Upmind\ProvisionProviders\SharedHosting\CentosWeb\Provider as CentosWeb; +use Upmind\ProvisionProviders\SharedHosting\Webuzo\Provider as Webuzo; class LaravelServiceProvider extends ProvisionServiceProvider { @@ -34,5 +35,6 @@ public function boot() $this->bindProvider('shared-hosting', 'solidcp', SolidCP::class); $this->bindProvider('shared-hosting', 'direct-admin', DirectAdmin::class); $this->bindProvider('shared-hosting', 'centos-web', CentosWeb::class); + $this->bindProvider('shared-hosting', 'webuzo', Webuzo::class); } } diff --git a/src/Webuzo/Api.php b/src/Webuzo/Api.php new file mode 100644 index 0000000..7d8fb3d --- /dev/null +++ b/src/Webuzo/Api.php @@ -0,0 +1,319 @@ +client = $client; + $this->configuration = $configuration; + } + + /** + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + */ + public function makeRequest( + string $command, + ?array $body = null, + ?string $method = 'POST' + ): ?array + { + $requestParams = []; + + if ($command == 'sso') { + $requestParams['query']['loginAs'] = $body['username']; + $requestParams['query']['noip'] = 1; + } + + $requestParams['query']['api'] = 'json'; + $requestParams['query']['act'] = $command; + + if ($body) { + $requestParams['form_params'] = $body; + } + + if (isset($this->configuration->api_key)) { + $requestParams['form_params']['apikey'] = $this->configuration->api_key; + + if (isset($this->configuration->username)) { + $requestParams['form_params']['apiuser'] = $this->configuration->username; + } else { + $requestParams['form_params']['apiuser'] = 'root'; + } + } + + $response = $this->client->request($method, '/index.php', $requestParams); + $result = $response->getBody()->getContents(); + + $response->getBody()->close(); + + if ($result === "") { + return null; + } + + return $this->parseResponseData($result); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + */ + private function parseResponseData(string $response): array + { + $parsedResult = json_decode($response, true); + + if ($error = $this->getResponseErrorMessage($parsedResult)) { + throw ProvisionFunctionError::create($error) + ->withData([ + 'response' => $response, + ]); + } + + return $parsedResult; + } + + private function getResponseErrorMessage(array $response): ?string + { + if (isset($response['error'])) { + $message = ''; + foreach ($response['error'] as $error) { + $message .= strip_tags($error) . '; '; + } + return $message; + } + + return null; + } + + public function createAccount(CreateParams $params, string $username, bool $asReseller): void + { + $password = $params->password ?: Helper::generatePassword(); + + $body = [ + 'create_user' => 1, + 'user' => $username, + 'user_passwd' => $password, + 'cnf_user_passwd' => $password, + 'domain' => $params->domain, + 'email' => $params->email, + 'plan' => $params->package_name, + ]; + + $this->makeRequest('add_user', $body); + + if ($asReseller) { + $this->setReseller($username, 1); + } + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function getAccountData(string $username): array + { + $account = $this->getUserDetails($username); + + return [ + 'username' => $username, + 'domain' => $account['domain'] ?? null, + 'reseller' => $account['type'] == 2, + 'server_hostname' => $this->configuration->hostname, + 'package_name' => $account['plan'] != "" ? $account['plan'] : "Unknown", + 'suspended' => $account['status'] === 'suspended', + 'suspend_reason' => $account['suspend_reason'] ?? null, + 'ip' => $account['ip'] ?? null, + ]; + } + + /** + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + */ + public function getUserDetails(string $username): ?array + { + $body = [ + 'search' => $username, + ]; + + $response = $this->makeRequest('users', $body); + + foreach ($response['users'] as $name => $account) { + if ($name === trim($username)) { + return $account; + } + } + + throw ProvisionFunctionError::create("User does not exist") + ->withData([ + 'username' => $username, + ]); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function getAccountUsage(string $username): UsageData + { + $account = $this->getUserDetails($username)['resource']; + + $disk = UnitsConsumed::create() + ->setUsed((int)$account['disk']['used_bytes'] / (1024 * 1024)) + ->setLimit(($account['disk']['limit_bytes'] == 0 || $account['disk']['limit_bytes'] == 'unlimited') + ? null : (int)$account['disk']['limit_bytes'] / (1024 * 1024)); + + $bandwidth = UnitsConsumed::create() + ->setUsed((int)$account['bandwidth']['used_bytes'] / (1024 * 1024)) + ->setLimit(($account['bandwidth']['limit_bytes'] == 0 || $account['bandwidth']['limit_bytes'] == 'unlimited') + ? null : (int)$account['bandwidth']['limit_bytes'] / (1024 * 1024)); + + $inodes = UnitsConsumed::create() + ->setUsed((int)$account['inode']['used']) + ->setLimit($account['inode']['limit'] == 'unlimited' + ? null : (int)$account['inode']['limit']); + + $mailboxes = UnitsConsumed::create() + ->setUsed((int)$account['email_account']['used']) + ->setLimit($account['email_account']['limit'] == 'unlimited' + ? null : (int)$account['email_account']['limit']); + + return UsageData::create() + ->setDiskMb($disk) + ->setBandwidthMb($bandwidth) + ->setInodes($inodes) + ->setMailboxes($mailboxes); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function suspendAccount(string $username): void + { + $body = [ + 'suspend' => $username + ]; + + $this->makeRequest('users', $body); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function unsuspendAccount(string $username): void + { + $body = [ + 'unsuspend' => $username + ]; + + $this->makeRequest('users', $body); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function deleteAccount(string $username): void + { + $body = [ + 'delete_user' => $username + ]; + + $this->makeRequest('users', $body); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function updatePackage(string $username, string $package): void + { + $account = $this->getUserDetails($username); + + $body = [ + 'edit_user' => 1, + 'user' => $username, + 'user_name' => $username, + 'domain' => $account['domain'], + 'email' => $account['email'], + 'plan' => $package + ]; + + $this->makeRequest('add_user', $body); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function updatePassword(string $username, string $password): void + { + $account = $this->getUserDetails($username); + + $body = [ + 'edit_user' => 1, + 'user' => $username, + 'user_name' => $username, + 'domain' => $account['domain'], + 'email' => $account['email'], + 'plan' => $account['plan'], + 'user_passwd' => $password, + 'cnf_user_passwd' => $password + ]; + + $this->makeRequest('add_user', $body); + } + + /** + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \RuntimeException + */ + public function getLoginUrl(string $username): string + { + $body = [ + 'username' => $username, + ]; + + $response = $this->makeRequest('sso', $body); + + return $response['done']['url']; + } + + public function setReseller(string $username, int $isReseller) + { + $account = $this->getUserDetails($username); + + $body = [ + 'edit_user' => 1, + 'user' => $username, + 'user_name' => $username, + 'domain' => $account['domain'], + 'email' => $account['email'], + 'plan' => $account['plan'], + 'reseller' => $isReseller, + ]; + + $this->makeRequest('add_user', $body); + } +} diff --git a/src/Webuzo/Data/Configuration.php b/src/Webuzo/Data/Configuration.php new file mode 100644 index 0000000..e6ebba4 --- /dev/null +++ b/src/Webuzo/Data/Configuration.php @@ -0,0 +1,28 @@ + ['required', 'string'], + 'api_key' => ['required_without_all:username,password', 'nullable', 'string'], + 'username' => ['required_without:api_key', 'nullable', 'string'], + 'password' => ['required_without:api_key', 'nullable', 'string'], + ]); + } +} diff --git a/src/Webuzo/Provider.php b/src/Webuzo/Provider.php new file mode 100644 index 0000000..47f6052 --- /dev/null +++ b/src/Webuzo/Provider.php @@ -0,0 +1,294 @@ +configuration = $configuration; + } + + /** + * @inheritDoc + */ + public static function aboutProvider(): AboutData + { + return AboutData::create() + ->setName('Webuzo') + ->setDescription('Create and manage Webuzo accounts and resellers using the Webuzo API') + ->setLogoUrl('https://api.upmind.io/images/logos/provision/webuzo-logo.png'); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function create(CreateParams $params): AccountInfo + { + if (!$params->domain) { + $this->errorResult('Domain name is required'); + } + + $asReseller = boolval($params->as_reseller ?? false); + + $username = $params->username ?: $this->generateUsername($params->domain); + + $this->api()->createAccount( + $params, + $username, + $asReseller + ); + + return $this->_getInfo($username, 'Account created'); + } + + protected function generateUsername(string $base): string + { + return substr( + preg_replace('/^[^a-z]+/', '', preg_replace('/[^a-z0-9]/', '', strtolower($base))), + 0, + self::MAX_USERNAME_LENGTH + ); + } + + /** + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + */ + protected function _getInfo(string $username, string $message): AccountInfo + { + $info = $this->api()->getAccountData($username); + + return AccountInfo::create($info)->setMessage($message); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function getInfo(AccountUsername $params): AccountInfo + { + return $this->_getInfo( + $params->username, + 'Account info retrieved', + ); + } + + /** + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function getUsage(AccountUsername $params): AccountUsage + { + $usage = $this->api()->getAccountUsage($params->username); + + return AccountUsage::create() + ->setUsageData($usage); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function getLoginUrl(GetLoginUrlParams $params): LoginUrl + { + $loginUrl = $this->api()->getLoginUrl($params->username); + + if (str_contains($loginUrl, 'webuzo.whgi.net')) { + $loginUrl = str_replace('webuzo.whgi.net', $this->configuration->hostname, $loginUrl); + } + + return LoginUrl::create() + ->setLoginUrl($loginUrl); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function changePassword(ChangePasswordParams $params): EmptyResult + { + $this->api()->updatePassword($params->username, $params->password); + + return $this->emptyResult('Password changed'); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function changePackage(ChangePackageParams $params): AccountInfo + { + $this->api()->updatePackage($params->username, $params->package_name); + + return $this->_getInfo( + $params->username, + 'Package changed' + ); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function suspend(SuspendParams $params): AccountInfo + { + $this->api()->suspendAccount($params->username); + + return $this->_getInfo($params->username, 'Account suspended'); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function unSuspend(AccountUsername $params): AccountInfo + { + $this->api()->unsuspendAccount($params->username); + + return $this->_getInfo($params->username, 'Account unsuspended'); + } + + /** + * @inheritDoc + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + public function terminate(AccountUsername $params): EmptyResult + { + $this->api()->deleteAccount($params->username); + + return $this->emptyResult('Account deleted'); + } + + /** + * @inheritDoc + * + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + */ + public function grantReseller(GrantResellerParams $params): ResellerPrivileges + { + $this->api()->setReseller($params->username, 1); + + return ResellerPrivileges::create() + ->setMessage('Reseller privileges granted') + ->setReseller(true); + } + + /** + * @inheritDoc + * + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + */ + public function revokeReseller(AccountUsername $params): ResellerPrivileges + { + $this->api()->setReseller($params->username, 0); + + return ResellerPrivileges::create() + ->setMessage('Reseller privileges revoked') + ->setReseller(false); + } + + /** + * @return no-return + * + * @throws \Upmind\ProvisionBase\Exception\ProvisionFunctionError + * @throws \Throwable + */ + protected function handleException(Throwable $e): void + { + // let the provision system handle this one + throw $e; + } + + protected function api(): Api + { + if ($this->api) { + return $this->api; + } + + $auth = ''; + + if (isset($this->configuration->username) && isset($this->configuration->password)) { + $auth = $this->configuration->username . ':' . $this->configuration->password . '@'; + } + + $client = new Client([ + 'base_uri' => sprintf('https://%s%s:%s', $auth, $this->configuration->hostname, 2005), + 'headers' => [ + 'Accept' => 'application/json', + ], + 'connect_timeout' => 10, + 'timeout' => 60, + 'verify' => false, + 'http_errors' => true, + 'allow_redirects' => false, + 'handler' => $this->getGuzzleHandlerStack(), + ]); + + return $this->api = new Api($client, $this->configuration); + } +}