From b196483a12c3b7c210b4d254f1e2b229fa1db1f0 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:26:17 +1030 Subject: [PATCH 01/19] Bugfixes --- README.md | 48 ++- src/HestiaCP/Authorization/Credentials.php | 8 +- src/HestiaCP/Client.php | 53 +++- .../Command/Add/LetsEncryptDomain.php | 4 + src/HestiaCP/Command/Backup/User.php | 9 +- src/HestiaCP/Command/Backup/Users.php | 13 +- src/HestiaCP/Command/Command.php | 75 ++++- src/HestiaCP/Command/ListCommand.php | 18 +- src/HestiaCP/Command/Lists/Databases.php | 5 - src/HestiaCP/Command/Lists/MailDomainDkim.php | 2 +- .../Command/Lists/MailDomainDkimDns.php | 2 +- src/HestiaCP/Command/ProcessCommand.php | 5 +- src/HestiaCP/Command/ResponseCode.php | 17 +- src/HestiaCP/Command/TestAuthorization.php | 20 +- src/HestiaCP/Module/Backups.php | 4 +- src/HestiaCP/Module/Databases.php | 5 +- src/HestiaCP/Module/Users.php | 4 +- tests/clientSendTest.php | 275 ++++++++++++++++++ tests/commandBackupTest.php | 6 +- tests/commandListTest.php | 27 +- tests/commandTest.php | 14 +- 21 files changed, 512 insertions(+), 102 deletions(-) create mode 100644 tests/clientSendTest.php diff --git a/README.md b/README.md index b3fe0be..e48fcd9 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,32 @@ use neto737\HestiaCP\Client; // Easy way to create Client -// Using API Key +// Using an access key (HestiaCP 1.6+), written as ACCESS_KEY:SECRET_KEY +$client = Client::simpleFactory('https://someHost', 'ACCESS_KEY:SECRET_KEY'); + +// Using a legacy API key $client = Client::simpleFactory('https://someHost', 'API_Key'); -// Using username and password -$client = Client::simpleFactory('https://someHost', 'someUser', 'somePass'); +// Using username and password (HestiaCP only accepts the admin/root user) +$client = Client::simpleFactory('https://someHost', 'admin', 'somePass'); + +``` + +The API has to be enabled in the HestiaCP server settings, and the IP address of the machine that calls it has to be allowed. + +Client options can be passed as the last argument of `simpleFactory()` or `new Client()`: + +```php + +$client = Client::simpleFactory('https://someHost', 'ACCESS_KEY:SECRET_KEY', null, [ + 'timeout' => 30, // seconds, default 10 + 'verify' => false, // default true; set to false for a self-signed certificate, or give a path to a CA bundle +]); ``` +> TLS certificates are verified by default. Older versions of this library never checked them. If your panel uses the self-signed certificate created at install, either install a valid certificate (for example with Let's Encrypt) or pass `'verify' => false`. + b) For some reasons (more hosts, etc) you may need create objects alone ```php @@ -60,11 +78,31 @@ $client = new Client($host); ```php -// verify login +// verify login (runs v-list-sys-info, so the credentials must be allowed to run it) $client->testAuthorization(); // bool ``` +Errors are thrown as exceptions. The HestiaCP exit code is available as the exception code (see `\neto737\HestiaCP\Command\ResponseCode`): + +```php + +use neto737\HestiaCP\AuthorizationException; +use neto737\HestiaCP\ClientException; +use neto737\HestiaCP\ProcessException; + +try { + $client->getModuleUser()->add('other_user', 'pa$$word', 'some@email.com'); +} catch (AuthorizationException $e) { + // bad credentials, or not allowed to run the command (exit code 9 or 10) +} catch (ProcessException $e) { + // the command failed, e.g. $e->getCode() === ResponseCode::E_EXISTS +} catch (ClientException $e) { + // connection problem (timeout, DNS, TLS, ...) +} + +``` + You can simply send one of prepared commands (or you can write own command - must implements `\neto737\HestiaCP\Command\ICommand` ) ```php @@ -212,7 +250,7 @@ f) backup module $backupModule = $client->getModuleBackup('admin'); // backup module needs user -$backupModule->backup(); // create a new backup +$backupModule->backup(); // create a new backup (waits until the backup is done, no timeout) $backupModule->delete('admin.2021-10-13_18-12-53.tar'); // delete an user backup diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php index 3feb43a..03a92fc 100644 --- a/src/HestiaCP/Authorization/Credentials.php +++ b/src/HestiaCP/Authorization/Credentials.php @@ -31,7 +31,7 @@ public function setApiKey(string $key) { return $this; } - public function getApiKey(): string { + public function getApiKey(): ?string { return $this->api_key; } @@ -40,7 +40,7 @@ public function setUser(string $user) { return $this; } - public function getUser(): string { + public function getUser(): ?string { return $this->user; } @@ -49,12 +49,12 @@ public function setPassword(string $password) { return $this; } - public function getPassword(): string { + public function getPassword(): ?string { return $this->password; } public function getRequestParams(): array { - if ($this->api_key) { + if ($this->api_key !== null) { return [ 'hash' => $this->getApiKey() ]; diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index e080417..9d9bd86 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -4,7 +4,7 @@ use neto737\HestiaCP\Authorization\Credentials; use neto737\HestiaCP\Authorization\Host; -use neto737\HestiaCP\Command\Add\LetsEncryptDomain; +use neto737\HestiaCP\Command\Command; use neto737\HestiaCP\Command\ICommand; use neto737\HestiaCP\Command\TestAuthorization; use neto737\HestiaCP\Module\Backups; @@ -26,6 +26,13 @@ class Client { /** * Client constructor. + * + * Options: + * - timeout (float) Request timeout in seconds, 0 = no timeout (default 10) + * - verify (bool|string) Verify the TLS certificate, or a path to a CA bundle (default true). + * Set to false for a server with a self-signed certificate. + * - handler (callable) Custom Guzzle handler stack (middleware, testing) + * * @param Host $host * @param array|null $options * @throws ClientException @@ -36,17 +43,18 @@ public function __construct(Host $host, array $options = null) { } /** - * @param string $hostname Hostname - * @param string $user Username or API Key - * @param mixed $password Password (must be null if using API Key) + * @param string $hostname Hostname + * @param string $user Username or API Key + * @param mixed $password Password (must be null if using API Key) + * @param array|null $options See Client::__construct() * @return Client * @throws ClientException */ - public static function simpleFactory(string $hostname, string $user, string $password = null): Client { + public static function simpleFactory(string $hostname, string $user, string $password = null, array $options = null): Client { $credentials = new Credentials($user, $password); $host = new Host($hostname, $credentials); - return new self($host); + return new self($host, $options); } public function setHost(Host $host) { @@ -77,10 +85,6 @@ public function testAuthorization(): bool { public function send(ICommand $command) { $host = $this->getHost(); try { - $timeout = $this->guzzleClient->getConfig('timeout'); - if ($command instanceof LetsEncryptDomain) { - $timeout = 90; - } $response = $this->guzzleClient->post($host->buildUri(), [ 'form_params' => array_merge( $host->getCredentials()->getRequestParams(), @@ -90,18 +94,31 @@ public function send(ICommand $command) { 'cmd' => $command->getName() ] ), - 'verify' => false, - 'timeout' => $timeout + 'timeout' => $this->getTimeout($command) ]); $command->setLastResponse($response); return $command->process(); } catch (ProcessException $e) { throw $e; - } catch (\GuzzleHttp\Exception\ClientException $e) { - return $command->processException($e); + } catch (\GuzzleHttp\Exception\GuzzleException $e) { + throw new ClientException('Connection error (' . $e->getMessage() . ')', 0, $e); } catch (\Exception $e) { - throw new ClientException('Fatal processing error (' . $e->getMessage() . ')'); + throw new ClientException('Fatal processing error (' . $e->getMessage() . ')', 0, $e); + } + } + + /** + * A command may need a longer timeout than the client default, but never a shorter one. + */ + private function getTimeout(ICommand $command): float { + $timeout = (float) $this->guzzleClient->getConfig('timeout'); + $commandTimeout = $command instanceof Command ? $command->getTimeout() : null; + + if ($commandTimeout === null || $timeout == 0) { + return $timeout; } + + return $commandTimeout == 0 ? 0 : max($timeout, $commandTimeout); } /** @@ -112,7 +129,11 @@ public function send(ICommand $command) { private function prepareCommunication(array $options = null): Client { try { $this->guzzleClient = new \GuzzleHttp\Client([ - 'timeout' => $options['timeout'] ?? 10.0 + 'timeout' => $options['timeout'] ?? 10.0, + 'verify' => $options['verify'] ?? true, + 'handler' => $options['handler'] ?? null, + // HestiaCP reports failures with HTTP error codes, the commands handle them + 'http_errors' => false ]); } catch (\Exception $e) { throw new ClientException('Bad Client configuration (' . $e->getMessage() . ')'); diff --git a/src/HestiaCP/Command/Add/LetsEncryptDomain.php b/src/HestiaCP/Command/Add/LetsEncryptDomain.php index 8ffea33..6c1974e 100644 --- a/src/HestiaCP/Command/Add/LetsEncryptDomain.php +++ b/src/HestiaCP/Command/Add/LetsEncryptDomain.php @@ -29,6 +29,10 @@ public function getName(): string { return 'v-add-letsencrypt-domain'; } + public function getTimeout(): ?float { + return 90; + } + public function getRequestParams(): array { return [ self::ARG_1 => $this->user, diff --git a/src/HestiaCP/Command/Backup/User.php b/src/HestiaCP/Command/Backup/User.php index 93b5d23..5c05b7b 100644 --- a/src/HestiaCP/Command/Backup/User.php +++ b/src/HestiaCP/Command/Backup/User.php @@ -2,9 +2,9 @@ namespace neto737\HestiaCP\Command\Backup; -use neto737\HestiaCP\Command\ListCommand; +use neto737\HestiaCP\Command\ProcessCommand; -class User extends ListCommand { +class User extends ProcessCommand { /** @var string */ private $user; @@ -21,6 +21,11 @@ public function getName(): string { return 'v-backup-user'; } + public function getTimeout(): ?float { + // the backup runs synchronously and can take a long time + return 0; + } + public function getRequestParams(): array { return [ self::ARG_1 => $this->user, diff --git a/src/HestiaCP/Command/Backup/Users.php b/src/HestiaCP/Command/Backup/Users.php index 9ea8644..e4a5e38 100644 --- a/src/HestiaCP/Command/Backup/Users.php +++ b/src/HestiaCP/Command/Backup/Users.php @@ -2,17 +2,20 @@ namespace neto737\HestiaCP\Command\Backup; -use neto737\HestiaCP\Command\ListCommand; +use neto737\HestiaCP\Command\ProcessCommand; -class Users extends ListCommand { +class Users extends ProcessCommand { public function getName(): string { return 'v-backup-users'; } + public function getTimeout(): ?float { + // the backup runs synchronously and can take a long time + return 0; + } + public function getRequestParams(): array { - return [ - self::ARG_1 => self::FORMAT_JSON - ]; + return []; } } diff --git a/src/HestiaCP/Command/Command.php b/src/HestiaCP/Command/Command.php index 39088c6..b534eba 100644 --- a/src/HestiaCP/Command/Command.php +++ b/src/HestiaCP/Command/Command.php @@ -19,32 +19,59 @@ abstract class Command implements ICommand { protected const FORMAT_JSON = 'json'; + /** @var string|null */ private $lastResponse; + /** @var int|null */ + private $lastStatusCode; + + /** @var int|null */ + private $lastExitCode; + /** + * @deprecated The client no longer lets Guzzle throw on HTTP errors, so this is not called anymore. * @param ClientException $exception * @throws ProcessException */ public function processException(ClientException $exception): void { - throw new ProcessException($exception->getMessage()); + throw new ProcessException($exception->getMessage(), 0, $exception); } public function setLastResponse(ResponseInterface $response): void { - $this->lastResponse = $response->getBody()->getContents(); + $this->lastResponse = (string) $response->getBody(); + $this->lastStatusCode = $response->getStatusCode(); + + $exitCode = $response->getHeaderLine('Hestia-Exit-Code'); + $this->lastExitCode = preg_match('~^\d+$~', $exitCode) ? (int) $exitCode : null; } public function getResponseText(): ?string { return $this->lastResponse; } + public function getStatusCode(): ?int { + return $this->lastStatusCode; + } + + /** + * Request timeout in seconds for this command (0 = no timeout), or null to use the client timeout. + */ + public function getTimeout(): ?float { + return null; + } + /** * @return int * @throws InvalidResponseException */ public function getResponseCode(): int { - $responseText = $this->getResponseText(); + if ($this->lastExitCode !== null) { + return $this->lastExitCode; + } + + $responseText = trim((string) $this->getResponseText()); - if (!preg_match('~^[\d]+$~', $responseText)) { + if (!preg_match('~^\d+$~', $responseText)) { throw new InvalidResponseException('Response is not code. Is hostname really HestiaCP?'); } @@ -53,21 +80,51 @@ public function getResponseCode(): int { /** * @throws AuthorizationException + * @throws ProcessException */ public function defaultProcess(): void { - $responseText = $this->getResponseText(); + $responseText = (string) $this->getResponseText(); - if ($responseText === 'Error: authentication failed') { - throw new AuthorizationException('Authorization failed! Bad user or password'); + if (strpos($responseText, 'Error: authentication failed') === 0) { + throw new AuthorizationException('Authorization failed! Bad user or password', ResponseCode::E_PASSWORD); + } + + // HestiaCP sends the command exit code in this header and an HTTP error status when it fails + if ($this->lastExitCode !== null) { + if ($this->lastExitCode !== ResponseCode::OK) { + throw $this->codeException($this->lastExitCode); + } + } elseif ($this->lastStatusCode !== null && $this->lastStatusCode >= 400) { + throw new InvalidResponseException('Unexpected HTTP status ' . $this->lastStatusCode . '. Is hostname really HestiaCP?'); } } /** - * @param $code + * @param int $code + * @throws AuthorizationException * @throws ProcessException */ protected function throwCodeException($code): void { - throw new ProcessException(ResponseCode::$messages[$code]); + throw $this->codeException((int) $code); + } + + /** + * Builds the exception for a non-zero HestiaCP exit code. + */ + protected function codeException(int $code): ProcessException { + $message = ResponseCode::getMessage($code); + + // with returncode=no the body holds HestiaCP's own error message + $detail = trim((string) $this->getResponseText()); + if ($detail !== '' && !preg_match('~^\d+$~', $detail)) { + $message .= ' (' . $detail . ')'; + } + + if ($code === ResponseCode::E_PASSWORD || $code === ResponseCode::E_FORBIDEN) { + return new AuthorizationException($message, $code); + } + + return new ProcessException($message, $code); } /** diff --git a/src/HestiaCP/Command/ListCommand.php b/src/HestiaCP/Command/ListCommand.php index 732c46e..6b92edd 100644 --- a/src/HestiaCP/Command/ListCommand.php +++ b/src/HestiaCP/Command/ListCommand.php @@ -6,6 +6,7 @@ use Nette\Utils\Json; use Nette\Utils\JsonException; use neto737\HestiaCP\AuthorizationException; +use neto737\HestiaCP\InvalidResponseException; use neto737\HestiaCP\ProcessException; abstract class ListCommand extends Command { @@ -22,11 +23,17 @@ public function needReturnCode(): bool { public function process() { parent::defaultProcess(); - $responseJson = $this->getResponseText(); + $responseJson = trim((string) $this->getResponseText()); + + // HestiaCP answers with an empty body (HTTP 204) when a command prints nothing + if ($responseJson === '') { + return []; + } + try { $response = Json::decode($responseJson); } catch (JsonException $e) { - throw new ProcessException($responseJson); + throw new ProcessException($responseJson, 0, $e); } // convert response to array @@ -41,7 +48,14 @@ protected function convertList(array $list): array { return $list; } + /** + * @throws InvalidResponseException + */ protected function convertDetail(array $list): ArrayHash { + if ($list === []) { + throw new InvalidResponseException('Empty response'); + } + return array_shift($list); } } diff --git a/src/HestiaCP/Command/Lists/Databases.php b/src/HestiaCP/Command/Lists/Databases.php index 127bb28..94cc2b7 100644 --- a/src/HestiaCP/Command/Lists/Databases.php +++ b/src/HestiaCP/Command/Lists/Databases.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Command\Lists; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Command\ListCommand; class Databases extends ListCommand { @@ -18,10 +17,6 @@ public function getName(): string { return 'v-list-databases'; } - public function process(): ArrayHash { - return $this->convertDetail(parent::process()); - } - public function getRequestParams(): array { return [ self::ARG_1 => $this->username, diff --git a/src/HestiaCP/Command/Lists/MailDomainDkim.php b/src/HestiaCP/Command/Lists/MailDomainDkim.php index 3758442..55fe32e 100644 --- a/src/HestiaCP/Command/Lists/MailDomainDkim.php +++ b/src/HestiaCP/Command/Lists/MailDomainDkim.php @@ -23,7 +23,7 @@ public function getName(): string { public function getResponseText(): string { // bug, issue 1827 ( https://github.com/serghey-rodin/vesta/issues/1827 ) - return preg_replace('~\,\s*\}~', '}', parent::getResponseText()); + return preg_replace('~\,\s*\}~', '}', (string) parent::getResponseText()); } public function getRequestParams(): array { diff --git a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php index bd19ce3..10db16c 100644 --- a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php +++ b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php @@ -23,7 +23,7 @@ public function getName(): string { public function getResponseText(): string { // bug, issue 1620 ( https://github.com/serghey-rodin/vesta/issues/1620 ) - return preg_replace('~\\n~', '', parent::getResponseText()); + return preg_replace('~\\n~', '', (string) parent::getResponseText()); } public function getRequestParams(): array { diff --git a/src/HestiaCP/Command/ProcessCommand.php b/src/HestiaCP/Command/ProcessCommand.php index cea4322..ab29780 100644 --- a/src/HestiaCP/Command/ProcessCommand.php +++ b/src/HestiaCP/Command/ProcessCommand.php @@ -20,10 +20,11 @@ public function needReturnCode(): bool { public function process(): bool { parent::defaultProcess(); - if ($this->getResponseCode() === 0) { + $code = $this->getResponseCode(); + if ($code === ResponseCode::OK) { return true; } - $this->throwCodeException($this->getResponseCode()); + throw $this->codeException($code); } } diff --git a/src/HestiaCP/Command/ResponseCode.php b/src/HestiaCP/Command/ResponseCode.php index 23bf696..e501fae 100644 --- a/src/HestiaCP/Command/ResponseCode.php +++ b/src/HestiaCP/Command/ResponseCode.php @@ -15,6 +15,7 @@ class ResponseCode { public const E_LIMIT = 8; public const E_PASSWORD = 9; public const E_FORBIDEN = 10; + public const E_FORBIDDEN = 10; public const E_DISABLED = 11; public const E_PARSING = 12; public const E_DISK = 13; @@ -25,9 +26,12 @@ class ResponseCode { public const E_RRD = 18; public const E_UPDATE = 19; public const E_RESTART = 20; + // HestiaCP uses 21 for both: E_BACKUP in the CLI scripts, E_API_DISABLED in the API + public const E_BACKUP = 21; + public const E_API_DISABLED = 21; public static $messages = [ - self::OK => 'Command has been successfuly performed', + self::OK => 'Command has been successfully performed', self::E_ARGS => 'Not enough arguments provided', self::E_INVALID => 'Object or argument is not valid', self::E_NOTEXIST => 'Object doesn\'t exist', @@ -37,16 +41,21 @@ class ResponseCode { self::E_INUSE => 'Object can\'t be deleted because is used by the other object', self::E_LIMIT => 'Object cannot be created because of hosting package limits', self::E_PASSWORD => 'Wrong password', - self::E_FORBIDEN => 'Object cannot be accessed be the user', + self::E_FORBIDEN => 'Object cannot be accessed by the user', self::E_DISABLED => 'Subsystem is disabled', self::E_PARSING => 'Configuration is broken', self::E_DISK => 'Not enough disk space to complete the action', - self::E_LA => 'Server is to busy to complete the action', + self::E_LA => 'Server is too busy to complete the action', self::E_CONNECT => 'Connection failed. Host is unreachable', self::E_FTP => 'FTP server is not responding', self::E_DB => 'Database server is not responding', self::E_RRD => 'RRDtool failed to update the database', self::E_UPDATE => 'Update operation failed', - self::E_RESTART => 'Service restart failed' + self::E_RESTART => 'Service restart failed', + self::E_BACKUP => 'Backup failed, or API access is disabled for this user' ]; + + public static function getMessage(int $code): string { + return self::$messages[$code] ?? 'Unknown error (exit code ' . $code . ')'; + } } diff --git a/src/HestiaCP/Command/TestAuthorization.php b/src/HestiaCP/Command/TestAuthorization.php index 0f61bb1..b235624 100644 --- a/src/HestiaCP/Command/TestAuthorization.php +++ b/src/HestiaCP/Command/TestAuthorization.php @@ -6,10 +6,14 @@ use neto737\HestiaCP\InvalidResponseException; use neto737\HestiaCP\ProcessException; +/** + * Runs a harmless read-only command to check the credentials. + * HestiaCP rejects an empty command before checking access keys, so a real command is needed. + */ class TestAuthorization extends Command { public function getName(): string { - return ''; + return 'v-list-sys-info'; } public function needReturnCode(): bool { @@ -18,20 +22,22 @@ public function needReturnCode(): bool { /** * @return bool + * @throws InvalidResponseException * @throws ProcessException */ public function process(): bool { try { parent::defaultProcess(); + + $code = $this->getResponseCode(); + if ($code === ResponseCode::OK) { + return true; + } + + throw $this->codeException($code); } catch (AuthorizationException $e) { return false; } - - if ($this->getResponseCode() === 1) { - return true; - } - - throw new InvalidResponseException('Invalid Response. Is host really HestiaCP?'); } public function getRequestParams(): array { diff --git a/src/HestiaCP/Module/Backups.php b/src/HestiaCP/Module/Backups.php index c886847..843ecdf 100644 --- a/src/HestiaCP/Module/Backups.php +++ b/src/HestiaCP/Module/Backups.php @@ -26,11 +26,11 @@ public function __construct(Client $client, string $user) { * This call is used for backing up user with all its domains and databases. * * @param bool $notify - * @return string + * @return bool * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ - public function backup(bool $notify = true): string { + public function backup(bool $notify = true): bool { return $this->client->send(new BackupUser($this->user, $notify)); } diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php index 9891b6a..41af976 100644 --- a/src/HestiaCP/Module/Databases.php +++ b/src/HestiaCP/Module/Databases.php @@ -74,12 +74,11 @@ public function listDatabase(string $database): ArrayHash { /** * This function for obtaining the list of all user's databases. * - * @param string $database - * @return ArrayHash + * @return ArrayHash[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ - public function listDatabases(): ArrayHash { + public function listDatabases(): array { return $this->client->send(new ListDatabases($this->user)); } } \ No newline at end of file diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index 6316228..9d2efd0 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -106,11 +106,11 @@ public function delete(string $user): bool { /** * This function backups all system users. * - * @return string + * @return bool * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ - public function backup(): string { + public function backup(): bool { return $this->client->send(new BackupUsers); } } diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php new file mode 100644 index 0000000..6eba116 --- /dev/null +++ b/tests/clientSendTest.php @@ -0,0 +1,275 @@ +history = []; + $stack = HandlerStack::create(new MockHandler($responses)); + $stack->push(Middleware::history($this->history)); + + return Client::simpleFactory('https://someHost', 'ACCESS:SECRET', null, $options + ['handler' => $stack]); + } + + private static function codeResponse(int $code, int $status = 200): Response { + return new Response($status, ['Hestia-Exit-Code' => (string) $code], (string) $code); + } + + public function testRequestFormat(): void { + $client = $this->createClient([self::codeResponse(0)]); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + + $request = $this->history[0]['request']; + $this->assertSame('POST', $request->getMethod()); + $this->assertSame('https://somehost:8083/api/', (string) $request->getUri()); + + parse_str((string) $request->getBody(), $params); + $this->assertSame([ + 'hash' => 'ACCESS:SECRET', + 'arg1' => 'user', + 'arg2' => 'pass', + 'arg3' => 'mail@domain.com', + 'returncode' => 'yes', + 'cmd' => 'v-add-user' + ], $params); + } + + public function testVerifyTlsByDefault(): void { + $client = $this->createClient([self::codeResponse(0)]); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + $this->assertTrue($this->history[0]['options']['verify']); + } + + public function testVerifyTlsCanBeDisabled(): void { + $client = $this->createClient([self::codeResponse(0)], ['verify' => false]); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + $this->assertFalse($this->history[0]['options']['verify']); + } + + public function testCommandTimeout(): void { + $client = $this->createClient([self::codeResponse(0), self::codeResponse(0), self::codeResponse(0)]); + + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + $this->assertEquals(10, $this->history[0]['options']['timeout']); + + $client->send(new LetsEncryptDomain('user', 'domain.com')); + $this->assertEquals(90, $this->history[1]['options']['timeout']); + + $client->send(new \neto737\HestiaCP\Command\Backup\User('user')); + $this->assertEquals(0, $this->history[2]['options']['timeout']); + } + + public function testCommandTimeoutNeverShortensClientTimeout(): void { + $client = $this->createClient([self::codeResponse(0)], ['timeout' => 120]); + $client->send(new LetsEncryptDomain('user', 'domain.com')); + $this->assertEquals(120, $this->history[0]['options']['timeout']); + } + + public function testProcessSuccess(): void { + $client = $this->createClient([self::codeResponse(0)]); + $this->assertTrue($client->send(new AddUser('user', 'pass', 'mail@domain.com'))); + } + + public function testProcessSuccessLegacy(): void { + $client = $this->createClient([new Response(200, [], '0')]); + $this->assertTrue($client->send(new AddUser('user', 'pass', 'mail@domain.com'))); + } + + public function testProcessError(): void { + $client = $this->createClient([self::codeResponse(ResponseCode::E_EXISTS, 400)]); + + try { + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + $this->fail('ProcessException expected'); + } catch (ProcessException $e) { + $this->assertNotInstanceOf(AuthorizationException::class, $e); + $this->assertSame(ResponseCode::E_EXISTS, $e->getCode()); + $this->assertSame('Object already exists', $e->getMessage()); + } + } + + public function testProcessErrorLegacy(): void { + $client = $this->createClient([new Response(200, [], '3')]); + + $this->expectException(ProcessException::class); + $this->expectExceptionCode(ResponseCode::E_NOTEXIST); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + } + + public function testUnknownErrorCode(): void { + $client = $this->createClient([self::codeResponse(99, 400)]); + + $this->expectException(ProcessException::class); + $this->expectExceptionMessage('Unknown error (exit code 99)'); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + } + + public function testAuthorizationFailure(): void { + $client = $this->createClient([self::codeResponse(ResponseCode::E_PASSWORD, 401)]); + + $this->expectException(AuthorizationException::class); + $this->expectExceptionCode(ResponseCode::E_PASSWORD); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + } + + public function testAuthorizationFailureLegacyText(): void { + $client = $this->createClient([ + new Response(401, ['Hestia-Exit-Code' => '9'], 'Error: authentication failed') + ]); + + $this->expectException(AuthorizationException::class); + $client->send(new Users()); + } + + public function testListErrorKeepsHestiaMessage(): void { + $client = $this->createClient([ + new Response(400, ['Hestia-Exit-Code' => '3'], "Error: user someone doesn't exist") + ]); + + $this->expectException(ProcessException::class); + $this->expectExceptionMessage("Object doesn't exist (Error: user someone doesn't exist)"); + $client->send(new User('someone')); + } + + public function testUnexpectedHttpStatus(): void { + $client = $this->createClient([new Response(502, [], 'Bad Gateway')]); + + $this->expectException(InvalidResponseException::class); + $client->send(new Users()); + } + + public function testNotHestiaResponse(): void { + $client = $this->createClient([new Response(200, [], 'Hello')]); + + $this->expectException(InvalidResponseException::class); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + } + + public function testConnectionError(): void { + $client = $this->createClient([new ConnectException('Connection refused', new Request('POST', 'test'))]); + + try { + $client->send(new Users()); + $this->fail('ClientException expected'); + } catch (ClientException $e) { + $this->assertInstanceOf(ConnectException::class, $e->getPrevious()); + } + } + + public function testList(): void { + $client = $this->createClient([ + new Response(200, ['Hestia-Exit-Code' => '0'], '{"admin": {"NAME": "System Administrator"}, "user": {"NAME": "User"}}') + ]); + + $users = $client->send(new Users()); + $this->assertSame(['admin', 'user'], array_keys($users)); + $this->assertInstanceOf(ArrayHash::class, $users['admin']); + $this->assertSame('System Administrator', $users['admin']->NAME); + } + + public function testListDetail(): void { + $client = $this->createClient([new Response(200, [], '{"admin": {"NAME": "System Administrator"}}')]); + + $user = $client->send(new User('admin')); + $this->assertInstanceOf(ArrayHash::class, $user); + $this->assertSame('System Administrator', $user->NAME); + } + + public function testListEmpty(): void { + $client = $this->createClient([new Response(204, ['Hestia-Exit-Code' => '0'], '')]); + $this->assertSame([], $client->send(new Users())); + } + + public function testListInvalidJson(): void { + $client = $this->createClient([new Response(200, [], 'not json')]); + + $this->expectException(ProcessException::class); + $this->expectExceptionMessage('not json'); + $client->send(new Users()); + } + + public function testListDatabasesReturnsAll(): void { + $client = $this->createClient([ + new Response(200, [], '{"admin_one": {"DBUSER": "admin_one"}, "admin_two": {"DBUSER": "admin_two"}}') + ]); + + $databases = $client->getModuleDatabase('admin')->listDatabases(); + $this->assertSame(['admin_one', 'admin_two'], array_keys($databases)); + } + + public function testBackup(): void { + $client = $this->createClient([self::codeResponse(0), self::codeResponse(0)]); + + $this->assertTrue($client->getModuleBackup('admin')->backup()); + $this->assertTrue($client->getModuleUser()->backup()); + + parse_str((string) $this->history[1]['request']->getBody(), $params); + $this->assertSame('v-backup-users', $params['cmd']); + $this->assertArrayNotHasKey('arg1', $params); + } + + public function testAuthorizationValid(): void { + $client = $this->createClient([self::codeResponse(0)]); + $this->assertTrue($client->testAuthorization()); + + parse_str((string) $this->history[0]['request']->getBody(), $params); + $this->assertSame('v-list-sys-info', $params['cmd']); + } + + public function testAuthorizationValidLegacy(): void { + $client = $this->createClient([new Response(200, [], '0')]); + $this->assertTrue($client->testAuthorization()); + } + + public function testAuthorizationInvalid(): void { + $client = $this->createClient([ + self::codeResponse(ResponseCode::E_PASSWORD, 401), + self::codeResponse(ResponseCode::E_FORBIDEN, 401) + ]); + + $this->assertFalse($client->testAuthorization()); + $this->assertFalse($client->testAuthorization()); + } + + public function testAuthorizationApiDisabled(): void { + $client = $this->createClient([self::codeResponse(ResponseCode::E_DISABLED, 400)]); + + $this->expectException(ProcessException::class); + $this->expectExceptionCode(ResponseCode::E_DISABLED); + $client->testAuthorization(); + } + + public function testAuthorizationNotHestia(): void { + $client = $this->createClient([new Response(200, [], 'Hello')]); + + $this->expectException(InvalidResponseException::class); + $client->testAuthorization(); + } +} diff --git a/tests/commandBackupTest.php b/tests/commandBackupTest.php index f054b01..37615af 100644 --- a/tests/commandBackupTest.php +++ b/tests/commandBackupTest.php @@ -10,9 +10,9 @@ class commandBackupTest extends \PHPUnit\Framework\TestCase { public function testBackupUsers(): void { $command = new Users; $this->assertSame('v-backup-users', $command->getName()); - $this->assertSame(['arg1' => 'json'], $command->getRequestParams()); + $this->assertSame([], $command->getRequestParams()); - $this->expectException(TypeError::class); + $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); $command->process(); } @@ -21,7 +21,7 @@ public function testBackupUser(): void { $this->assertSame('v-backup-user', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'yes'], $command->getRequestParams()); - $this->expectException(TypeError::class); + $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); $command->process(); } } diff --git a/tests/commandListTest.php b/tests/commandListTest.php index a8e1a1d..e95fe41 100644 --- a/tests/commandListTest.php +++ b/tests/commandListTest.php @@ -24,7 +24,7 @@ public function testUser(): void { $this->assertFalse($command->needReturnCode()); - $this->expectException(TypeError::class); + $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); $command->process(); } @@ -33,8 +33,7 @@ public function testUsers(): void { $this->assertSame('v-list-users', $command->getName()); $this->assertSame(['arg1' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } public function testWebDomains(): void { @@ -42,8 +41,7 @@ public function testWebDomains(): void { $this->assertSame('v-list-web-domains', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } public function testMailDomains(): void { @@ -51,8 +49,7 @@ public function testMailDomains(): void { $this->assertSame('v-list-mail-domains', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } public function testMailDomainDkim(): void { @@ -74,8 +71,7 @@ public function testMailAccounts(): void { $this->assertSame('v-list-mail-accounts', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'domain', 'arg3' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } public function testUserBackups(): void { @@ -83,8 +79,7 @@ public function testUserBackups(): void { $this->assertSame('v-list-user-backups', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } public function testUserBackup(): void { @@ -92,7 +87,7 @@ public function testUserBackup(): void { $this->assertSame('v-list-user-backup', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'admin.2021-10-07_05-15-09.tar', 'arg3' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); + $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); $command->process(); } @@ -101,8 +96,7 @@ public function testUserBackupExclusions(): void { $this->assertSame('v-list-user-backup-exclusions', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } public function testListDatabase(): void { @@ -110,7 +104,7 @@ public function testListDatabase(): void { $this->assertSame('v-list-database', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'test', 'arg3' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); + $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); $command->process(); } @@ -119,7 +113,6 @@ public function testListDatabases(): void { $this->assertSame('v-list-databases', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); - $this->expectException(TypeError::class); - $command->process(); + $this->assertSame([], $command->process()); } } diff --git a/tests/commandTest.php b/tests/commandTest.php index 400f1f9..f3b0724 100644 --- a/tests/commandTest.php +++ b/tests/commandTest.php @@ -2,7 +2,6 @@ use neto737\HestiaCP\Command\TestAuthorization; use neto737\HestiaCP\InvalidResponseException; -use neto737\HestiaCP\ProcessException; require_once __DIR__ . '/../vendor/autoload.php'; @@ -10,22 +9,13 @@ class commandTest extends \PHPUnit\Framework\TestCase { public function testCommands(): void { $command = new TestAuthorization(); - $this->assertSame('', $command->getName()); + $this->assertSame('v-list-sys-info', $command->getName()); $this->assertSame([], $command->getRequestParams()); $this->assertTrue($command->needReturnCode()); - - $this->expectException(InvalidResponseException::class); - $command->process(); - $this->assertNull($command->getResponseText()); $this->expectException(InvalidResponseException::class); - $this->assertNull($command->getResponseCode()); - - $command->defaultProcess(); - - $this->expectException(ProcessException::class); - $command->processException(new \GuzzleHttp\Exception\ClientException('throw!')); + $command->process(); } } From c8843dafe4a88079bf886617edfc953c564561c7 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:33:23 +1030 Subject: [PATCH 02/19] Removed nette/utils dependency --- README.md | 13 ++++++++++ composer.json | 4 +-- src/HestiaCP/Command/ListCommand.php | 30 ++++++++++------------- src/HestiaCP/Command/Lists/Database.php | 3 +-- src/HestiaCP/Command/Lists/User.php | 3 +-- src/HestiaCP/Command/Lists/UserBackup.php | 3 +-- src/HestiaCP/Module/Backups.php | 5 ++-- src/HestiaCP/Module/Databases.php | 7 +++--- src/HestiaCP/Module/Mails.php | 9 +++---- src/HestiaCP/Module/Users.php | 5 ++-- src/HestiaCP/Module/Webs.php | 3 +-- tests/clientSendTest.php | 25 ++++++++++++++----- 12 files changed, 62 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index e48fcd9..d5b2e8a 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ echo $response->getResponseText(); Or you can use prepared modules +List and detail methods return plain associative arrays decoded from HestiaCP's JSON, keyed by the object name: + +```php + +$users = $client->getModuleUser()->list(); // ['admin' => ['NAME' => '...', 'CONTACT' => '...', ...], ...] + +$admin = $client->getModuleUser()->detail('admin'); +echo $admin['CONTACT']; + +``` + +> Before v2.0 these methods returned `Nette\Utils\ArrayHash` objects. To upgrade, replace `$item->FIELD` with `$item['FIELD']`. + a) user module ```php diff --git a/composer.json b/composer.json index 73b0f45..42cb85c 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,8 @@ "description": "HestiaCP API SDK written in PHP", "require": { "php": ">=7.3", - "guzzlehttp/guzzle": "^7.4", - "nette/utils": "^3.2" + "ext-json": "*", + "guzzlehttp/guzzle": "^7.4" }, "homepage": "https://github.com/neto737/HestiaCP-API", "license": "MIT", diff --git a/src/HestiaCP/Command/ListCommand.php b/src/HestiaCP/Command/ListCommand.php index 6b92edd..2eca527 100644 --- a/src/HestiaCP/Command/ListCommand.php +++ b/src/HestiaCP/Command/ListCommand.php @@ -2,9 +2,6 @@ namespace neto737\HestiaCP\Command; -use Nette\Utils\ArrayHash; -use Nette\Utils\Json; -use Nette\Utils\JsonException; use neto737\HestiaCP\AuthorizationException; use neto737\HestiaCP\InvalidResponseException; use neto737\HestiaCP\ProcessException; @@ -16,7 +13,7 @@ public function needReturnCode(): bool { } /** - * @return array|ArrayHash + * @return array * @throws ProcessException * @throws AuthorizationException */ @@ -31,31 +28,30 @@ public function process() { } try { - $response = Json::decode($responseJson); - } catch (JsonException $e) { + $response = json_decode($responseJson, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { throw new ProcessException($responseJson, 0, $e); } - // convert response to array - return $this->convertList((array)$response); - } - - protected function convertList(array $list): array { - foreach ($list as $key => $item) { - $list[$key] = ArrayHash::from((array) $item); + if (!is_array($response)) { + throw new ProcessException($responseJson); } - return $list; + return $response; } /** + * Returns the only item of a single-object list, e.g. {"admin": {...}}. + * * @throws InvalidResponseException */ - protected function convertDetail(array $list): ArrayHash { - if ($list === []) { + protected function convertDetail(array $list): array { + $detail = array_shift($list); + + if (!is_array($detail)) { throw new InvalidResponseException('Empty response'); } - return array_shift($list); + return $detail; } } diff --git a/src/HestiaCP/Command/Lists/Database.php b/src/HestiaCP/Command/Lists/Database.php index dfd7c03..12d203a 100644 --- a/src/HestiaCP/Command/Lists/Database.php +++ b/src/HestiaCP/Command/Lists/Database.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Command\Lists; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Command\ListCommand; class Database extends ListCommand { @@ -22,7 +21,7 @@ public function getName(): string { return 'v-list-database'; } - public function process(): ArrayHash { + public function process(): array { return $this->convertDetail(parent::process()); } diff --git a/src/HestiaCP/Command/Lists/User.php b/src/HestiaCP/Command/Lists/User.php index 07ee07a..4ab0c94 100644 --- a/src/HestiaCP/Command/Lists/User.php +++ b/src/HestiaCP/Command/Lists/User.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Command\Lists; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Command\ListCommand; class User extends ListCommand { @@ -18,7 +17,7 @@ public function getName(): string { return 'v-list-user'; } - public function process(): ArrayHash { + public function process(): array { return $this->convertDetail(parent::process()); } diff --git a/src/HestiaCP/Command/Lists/UserBackup.php b/src/HestiaCP/Command/Lists/UserBackup.php index 5fba325..3f0b5c8 100644 --- a/src/HestiaCP/Command/Lists/UserBackup.php +++ b/src/HestiaCP/Command/Lists/UserBackup.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Command\Lists; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Command\ListCommand; class UserBackup extends ListCommand { @@ -22,7 +21,7 @@ public function getName(): string { return 'v-list-user-backup'; } - public function process(): ArrayHash { + public function process(): array { return $this->convertDetail(parent::process()); } diff --git a/src/HestiaCP/Module/Backups.php b/src/HestiaCP/Module/Backups.php index 843ecdf..15716ff 100644 --- a/src/HestiaCP/Module/Backups.php +++ b/src/HestiaCP/Module/Backups.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Module; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Client; use neto737\HestiaCP\Command\Backup\User as BackupUser; @@ -72,11 +71,11 @@ public function listBackups(): array { * This function of obtaining the list of backup parameters. * * @param string $backup - * @return ArrayHash + * @return array * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ - public function listBackup(string $backup): ArrayHash { + public function listBackup(string $backup): array { return $this->client->send(new ListsUserBackup($this->user, $backup)); } diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php index 41af976..5d5957d 100644 --- a/src/HestiaCP/Module/Databases.php +++ b/src/HestiaCP/Module/Databases.php @@ -3,7 +3,6 @@ namespace neto737\HestiaCP\Module; use neto737\HestiaCP\Client; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Command\Add\Database as AddDatabase; use neto737\HestiaCP\Command\Delete\Database as DeleteDatabase; @@ -63,18 +62,18 @@ public function deleteDatabases(): bool { * This function for obtaining of all database's parameters. * * @param string $database - * @return ArrayHash + * @return array * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ - public function listDatabase(string $database): ArrayHash { + public function listDatabase(string $database): array { return $this->client->send(new ListDatabase($this->user, $database)); } /** * This function for obtaining the list of all user's databases. * - * @return ArrayHash[] + * @return array[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ diff --git a/src/HestiaCP/Module/Mails.php b/src/HestiaCP/Module/Mails.php index dcc39a3..ef33428 100644 --- a/src/HestiaCP/Module/Mails.php +++ b/src/HestiaCP/Module/Mails.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Module; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Client; use neto737\HestiaCP\Command\Add\MailAccount; use neto737\HestiaCP\Command\Add\MailDomain; @@ -32,7 +31,7 @@ public function __construct(Client $client, string $user) { * This function of obtaining the list of all user domains. * * @param string $domain - * @return ArrayHash[] + * @return array[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ @@ -162,7 +161,7 @@ public function deleteDomain(string $domain): bool { * This function of obtaining domain dkim files. * * @param string $domain - * @return ArrayHash[] + * @return array[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ @@ -174,7 +173,7 @@ public function listDomainDkim(string $domain): array { * This function of obtaining domain dkim dns records for proper setup. * * @param string $domain - * @return ArrayHash[] + * @return array[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ @@ -185,7 +184,7 @@ public function listDomainDkimDns(string $domain): array { /** * This function of obtaining the list of all user domains. * - * @return ArrayHash[] + * @return array[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index 9d2efd0..aefa786 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Module; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Command\Add\User as AddUser; use neto737\HestiaCP\Command\Change\UserPassword; use neto737\HestiaCP\Command\Suspend\User as SuspendUser; @@ -29,11 +28,11 @@ public function list(): array { * This function to obtain user parameters. * * @param string $user - * @return ArrayHash + * @return array * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ - public function detail(string $user): ArrayHash { + public function detail(string $user): array { return $this->client->send(new User($user)); } diff --git a/src/HestiaCP/Module/Webs.php b/src/HestiaCP/Module/Webs.php index 38a01bb..b3b632d 100644 --- a/src/HestiaCP/Module/Webs.php +++ b/src/HestiaCP/Module/Webs.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Module; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\Client; use neto737\HestiaCP\Command\Add\LetsEncryptDomain; use neto737\HestiaCP\Command\Add\WebDomain; @@ -166,7 +165,7 @@ public function deleteDomainFtp(string $domain, string $ftpUser): bool { /** * This function to obtain the list of all user web domains. * - * @return ArrayHash[] + * @return array[] * @throws \neto737\HestiaCP\ClientException * @throws \neto737\HestiaCP\ProcessException */ diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index 6eba116..dac8258 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -6,7 +6,6 @@ use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use Nette\Utils\ArrayHash; use neto737\HestiaCP\AuthorizationException; use neto737\HestiaCP\Client; use neto737\HestiaCP\ClientException; @@ -189,17 +188,31 @@ public function testList(): void { ]); $users = $client->send(new Users()); - $this->assertSame(['admin', 'user'], array_keys($users)); - $this->assertInstanceOf(ArrayHash::class, $users['admin']); - $this->assertSame('System Administrator', $users['admin']->NAME); + $this->assertSame([ + 'admin' => ['NAME' => 'System Administrator'], + 'user' => ['NAME' => 'User'] + ], $users); } public function testListDetail(): void { $client = $this->createClient([new Response(200, [], '{"admin": {"NAME": "System Administrator"}}')]); $user = $client->send(new User('admin')); - $this->assertInstanceOf(ArrayHash::class, $user); - $this->assertSame('System Administrator', $user->NAME); + $this->assertSame(['NAME' => 'System Administrator'], $user); + } + + public function testListDetailNotObject(): void { + $client = $this->createClient([new Response(200, [], '{"admin": "System Administrator"}')]); + + $this->expectException(InvalidResponseException::class); + $client->send(new User('admin')); + } + + public function testListNotList(): void { + $client = $this->createClient([new Response(200, [], '"OK"')]); + + $this->expectException(ProcessException::class); + $client->send(new Users()); } public function testListEmpty(): void { From 3b53d6b30ea21a4b9b4e99807fb68e69651726b9 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:35:51 +1030 Subject: [PATCH 03/19] Set minimum-stability to stable --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 42cb85c..0dcc2dc 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "email": "neto737@live.com" } ], - "minimum-stability": "dev", + "minimum-stability": "stable", "autoload": { "classmap": ["src/"] }, From ea00a2869d039cd06f67643a92c36f31acc3037a Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:42:27 +1030 Subject: [PATCH 04/19] - Updated PHP to 8.1+ - Switched from classmap to PSR-4 loading - Replaced implicit nullable params with "?string" format --- README.md | 6 +-- composer.json | 6 ++- src/HestiaCP/Authorization/Credentials.php | 2 +- src/HestiaCP/Client.php | 8 +-- .../Command/Add/LetsEncryptDomain.php | 2 +- src/HestiaCP/Command/Add/User.php | 2 +- src/HestiaCP/Command/Add/WebDomain.php | 2 +- src/HestiaCP/Command/Add/WebDomainFtp.php | 2 +- src/HestiaCP/Command/Command.php | 6 +-- src/HestiaCP/Command/ListCommand.php | 6 +-- src/HestiaCP/Command/ProcessCommand.php | 4 +- src/HestiaCP/Command/TestAuthorization.php | 6 +-- src/HestiaCP/Exceptions.php | 17 ------ .../Exceptions/AuthorizationException.php | 5 ++ src/HestiaCP/Exceptions/ClientException.php | 5 ++ src/HestiaCP/Exceptions/Exception.php | 5 ++ .../Exceptions/InvalidArgumentException.php | 5 ++ .../Exceptions/InvalidResponseException.php | 5 ++ src/HestiaCP/Exceptions/ProcessException.php | 5 ++ src/HestiaCP/Exceptions/RuntimeException.php | 5 ++ src/HestiaCP/Module/Backups.php | 24 ++++----- src/HestiaCP/Module/Databases.php | 20 +++---- src/HestiaCP/Module/Mails.php | 52 +++++++++---------- src/HestiaCP/Module/Users.php | 34 ++++++------ src/HestiaCP/Module/Webs.php | 50 +++++++++--------- tests/clientSendTest.php | 8 +-- tests/commandAddTest.php | 14 ++--- tests/commandBackupTest.php | 4 +- tests/commandChangeTest.php | 8 +-- tests/commandDeleteTest.php | 20 +++---- tests/commandListTest.php | 6 +-- tests/commandSuspendTest.php | 8 +-- tests/commandTest.php | 2 +- tests/commandUnsuspendTest.php | 8 +-- 34 files changed, 192 insertions(+), 170 deletions(-) delete mode 100644 src/HestiaCP/Exceptions.php create mode 100644 src/HestiaCP/Exceptions/AuthorizationException.php create mode 100644 src/HestiaCP/Exceptions/ClientException.php create mode 100644 src/HestiaCP/Exceptions/Exception.php create mode 100644 src/HestiaCP/Exceptions/InvalidArgumentException.php create mode 100644 src/HestiaCP/Exceptions/InvalidResponseException.php create mode 100644 src/HestiaCP/Exceptions/ProcessException.php create mode 100644 src/HestiaCP/Exceptions/RuntimeException.php diff --git a/README.md b/README.md index d5b2e8a..35019da 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,9 @@ Errors are thrown as exceptions. The HestiaCP exit code is available as the exce ```php -use neto737\HestiaCP\AuthorizationException; -use neto737\HestiaCP\ClientException; -use neto737\HestiaCP\ProcessException; +use neto737\HestiaCP\Exceptions\AuthorizationException; +use neto737\HestiaCP\Exceptions\ClientException; +use neto737\HestiaCP\Exceptions\ProcessException; try { $client->getModuleUser()->add('other_user', 'pa$$word', 'some@email.com'); diff --git a/composer.json b/composer.json index 0dcc2dc..ffb17ab 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "neto737/hestiacp-api", "description": "HestiaCP API SDK written in PHP", "require": { - "php": ">=7.3", + "php": ">=8.1", "ext-json": "*", "guzzlehttp/guzzle": "^7.4" }, @@ -16,7 +16,9 @@ ], "minimum-stability": "stable", "autoload": { - "classmap": ["src/"] + "psr-4": { + "neto737\\HestiaCP\\": "src/HestiaCP/" + } }, "require-dev": { "phpunit/phpunit": "^9" diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php index 03a92fc..94661aa 100644 --- a/src/HestiaCP/Authorization/Credentials.php +++ b/src/HestiaCP/Authorization/Credentials.php @@ -17,7 +17,7 @@ class Credentials { * @param string $user Username or Api Key * @param string @password Password (must be null if using API Key) */ - public function __construct(string $user, string $password = null) { + public function __construct(string $user, ?string $password = null) { if (is_null($password)) { $this->setApiKey($user); } else { diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index 9d9bd86..8962449 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -7,6 +7,8 @@ use neto737\HestiaCP\Command\Command; use neto737\HestiaCP\Command\ICommand; use neto737\HestiaCP\Command\TestAuthorization; +use neto737\HestiaCP\Exceptions\ClientException; +use neto737\HestiaCP\Exceptions\ProcessException; use neto737\HestiaCP\Module\Backups; use neto737\HestiaCP\Module\Databases; use neto737\HestiaCP\Module\Mails; @@ -37,7 +39,7 @@ class Client { * @param array|null $options * @throws ClientException */ - public function __construct(Host $host, array $options = null) { + public function __construct(Host $host, ?array $options = null) { $this->setHost($host); $this->prepareCommunication($options); } @@ -50,7 +52,7 @@ public function __construct(Host $host, array $options = null) { * @return Client * @throws ClientException */ - public static function simpleFactory(string $hostname, string $user, string $password = null, array $options = null): Client { + public static function simpleFactory(string $hostname, string $user, ?string $password = null, ?array $options = null): Client { $credentials = new Credentials($user, $password); $host = new Host($hostname, $credentials); @@ -126,7 +128,7 @@ private function getTimeout(ICommand $command): float { * @return Client * @throws ClientException */ - private function prepareCommunication(array $options = null): Client { + private function prepareCommunication(?array $options = null): Client { try { $this->guzzleClient = new \GuzzleHttp\Client([ 'timeout' => $options['timeout'] ?? 10.0, diff --git a/src/HestiaCP/Command/Add/LetsEncryptDomain.php b/src/HestiaCP/Command/Add/LetsEncryptDomain.php index 6c1974e..e082a59 100644 --- a/src/HestiaCP/Command/Add/LetsEncryptDomain.php +++ b/src/HestiaCP/Command/Add/LetsEncryptDomain.php @@ -18,7 +18,7 @@ class LetsEncryptDomain extends ProcessCommand { /** @var bool */ private $restart; - public function __construct(string $user, string $domain, string $aliases = null, bool $restart = false) { + public function __construct(string $user, string $domain, ?string $aliases = null, bool $restart = false) { $this->user = $user; $this->domain = $domain; $this->aliases = $aliases; diff --git a/src/HestiaCP/Command/Add/User.php b/src/HestiaCP/Command/Add/User.php index cd153c2..b852a9d 100644 --- a/src/HestiaCP/Command/Add/User.php +++ b/src/HestiaCP/Command/Add/User.php @@ -18,7 +18,7 @@ class User extends ProcessCommand { /** @var string */ private $package; - public function __construct(string $user, string $password, string $email, string $package = null) { + public function __construct(string $user, string $password, string $email, ?string $package = null) { $this->user = $user; $this->password = $password; $this->email = $email; diff --git a/src/HestiaCP/Command/Add/WebDomain.php b/src/HestiaCP/Command/Add/WebDomain.php index f9ee595..56aa66d 100644 --- a/src/HestiaCP/Command/Add/WebDomain.php +++ b/src/HestiaCP/Command/Add/WebDomain.php @@ -24,7 +24,7 @@ class WebDomain extends ProcessCommand { /** @var bool */ private $restart; - public function __construct(string $user, string $domain, string $ip = null, string $aliases = null, string $proxyExtensions = null, bool $restart = false) { + public function __construct(string $user, string $domain, ?string $ip = null, ?string $aliases = null, ?string $proxyExtensions = null, bool $restart = false) { $this->user = $user; $this->domain = $domain; $this->ip = $ip; diff --git a/src/HestiaCP/Command/Add/WebDomainFtp.php b/src/HestiaCP/Command/Add/WebDomainFtp.php index c9531a7..26e0dd2 100644 --- a/src/HestiaCP/Command/Add/WebDomainFtp.php +++ b/src/HestiaCP/Command/Add/WebDomainFtp.php @@ -21,7 +21,7 @@ class WebDomainFtp extends ProcessCommand { /** @var string|null */ private $ftpPath; - public function __construct(string $user, string $domain, string $ftpUser, string $ftpPassword, string $ftpPath = null) { + public function __construct(string $user, string $domain, string $ftpUser, string $ftpPassword, ?string $ftpPath = null) { $this->user = $user; $this->domain = $domain; $this->ftpUser = $ftpUser; diff --git a/src/HestiaCP/Command/Command.php b/src/HestiaCP/Command/Command.php index b534eba..70905eb 100644 --- a/src/HestiaCP/Command/Command.php +++ b/src/HestiaCP/Command/Command.php @@ -3,9 +3,9 @@ namespace neto737\HestiaCP\Command; use GuzzleHttp\Exception\ClientException; -use neto737\HestiaCP\AuthorizationException; -use neto737\HestiaCP\InvalidResponseException; -use neto737\HestiaCP\ProcessException; +use neto737\HestiaCP\Exceptions\AuthorizationException; +use neto737\HestiaCP\Exceptions\InvalidResponseException; +use neto737\HestiaCP\Exceptions\ProcessException; use Psr\Http\Message\ResponseInterface; abstract class Command implements ICommand { diff --git a/src/HestiaCP/Command/ListCommand.php b/src/HestiaCP/Command/ListCommand.php index 2eca527..04a970e 100644 --- a/src/HestiaCP/Command/ListCommand.php +++ b/src/HestiaCP/Command/ListCommand.php @@ -2,9 +2,9 @@ namespace neto737\HestiaCP\Command; -use neto737\HestiaCP\AuthorizationException; -use neto737\HestiaCP\InvalidResponseException; -use neto737\HestiaCP\ProcessException; +use neto737\HestiaCP\Exceptions\AuthorizationException; +use neto737\HestiaCP\Exceptions\InvalidResponseException; +use neto737\HestiaCP\Exceptions\ProcessException; abstract class ListCommand extends Command { diff --git a/src/HestiaCP/Command/ProcessCommand.php b/src/HestiaCP/Command/ProcessCommand.php index ab29780..fc3a73b 100644 --- a/src/HestiaCP/Command/ProcessCommand.php +++ b/src/HestiaCP/Command/ProcessCommand.php @@ -2,9 +2,9 @@ namespace neto737\HestiaCP\Command; -use neto737\HestiaCP\AuthorizationException; +use neto737\HestiaCP\Exceptions\AuthorizationException; use neto737\HestiaCP\Command\Command; -use neto737\HestiaCP\ProcessException; +use neto737\HestiaCP\Exceptions\ProcessException; abstract class ProcessCommand extends Command { diff --git a/src/HestiaCP/Command/TestAuthorization.php b/src/HestiaCP/Command/TestAuthorization.php index b235624..30cf3a9 100644 --- a/src/HestiaCP/Command/TestAuthorization.php +++ b/src/HestiaCP/Command/TestAuthorization.php @@ -2,9 +2,9 @@ namespace neto737\HestiaCP\Command; -use neto737\HestiaCP\AuthorizationException; -use neto737\HestiaCP\InvalidResponseException; -use neto737\HestiaCP\ProcessException; +use neto737\HestiaCP\Exceptions\AuthorizationException; +use neto737\HestiaCP\Exceptions\InvalidResponseException; +use neto737\HestiaCP\Exceptions\ProcessException; /** * Runs a harmless read-only command to check the credentials. diff --git a/src/HestiaCP/Exceptions.php b/src/HestiaCP/Exceptions.php deleted file mode 100644 index 91b5dd2..0000000 --- a/src/HestiaCP/Exceptions.php +++ /dev/null @@ -1,17 +0,0 @@ -client->send(new BackupUser($this->user, $notify)); @@ -38,8 +38,8 @@ public function backup(bool $notify = true): bool { * * @param string $backup * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function delete(string $backup): bool { return $this->client->send(new DeleteUserBackup($this->user, $backup)); @@ -49,8 +49,8 @@ public function delete(string $backup): bool { * This function for deleting backup exclusion * * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteExclusions(): bool { return $this->client->send(new DeleteUserBackupExclusions($this->user)); @@ -60,8 +60,8 @@ public function deleteExclusions(): bool { * This function for obtaining the list of available user backups. * * @return array - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listBackups(): array { return $this->client->send(new UserBackups($this->user)); @@ -72,8 +72,8 @@ public function listBackups(): array { * * @param string $backup * @return array - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listBackup(string $backup): array { return $this->client->send(new ListsUserBackup($this->user, $backup)); @@ -83,8 +83,8 @@ public function listBackup(string $backup): array { * This function for obtaining the backup exclusion list * * @return array - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listBackupExclusions(): array { return $this->client->send(new ListsUserBackupExclusions($this->user)); diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php index 5d5957d..877b7b2 100644 --- a/src/HestiaCP/Module/Databases.php +++ b/src/HestiaCP/Module/Databases.php @@ -28,8 +28,8 @@ public function __construct(Client $client, string $user) { * @param string $dbpass * @param string $type * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function add(string $database, string $dbuser, string $dbpass, string $type = 'mysql'): bool { return $this->client->send(new AddDatabase($this->user, $database, $dbuser, $dbpass, $type)); @@ -40,8 +40,8 @@ public function add(string $database, string $dbuser, string $dbpass, string $ty * * @param string $database * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function delete(string $database): bool { return $this->client->send(new DeleteDatabase($this->user, $database)); @@ -51,8 +51,8 @@ public function delete(string $database): bool { * This function deletes all user databases. * * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteDatabases(): bool { return $this->client->send(new DeleteDatabases($this->user)); @@ -63,8 +63,8 @@ public function deleteDatabases(): bool { * * @param string $database * @return array - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listDatabase(string $database): array { return $this->client->send(new ListDatabase($this->user, $database)); @@ -74,8 +74,8 @@ public function listDatabase(string $database): array { * This function for obtaining the list of all user's databases. * * @return array[] - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listDatabases(): array { return $this->client->send(new ListDatabases($this->user)); diff --git a/src/HestiaCP/Module/Mails.php b/src/HestiaCP/Module/Mails.php index ef33428..71702aa 100644 --- a/src/HestiaCP/Module/Mails.php +++ b/src/HestiaCP/Module/Mails.php @@ -32,8 +32,8 @@ public function __construct(Client $client, string $user) { * * @param string $domain * @return array[] - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listAccounts(string $domain): array { return $this->client->send(new MailAccounts($this->user, $domain)); @@ -46,8 +46,8 @@ public function listAccounts(string $domain): array { * @param string $account * @param string $password * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function addAccount(string $domain, string $account, string $password): bool { return $this->client->send(new MailAccount($this->user, $domain, $account, $password)); @@ -60,8 +60,8 @@ public function addAccount(string $domain, string $account, string $password): b * @param string $account * @param string $password * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function changeAccountPassword(string $domain, string $account, string $password): bool { return $this->client->send(new MailAccountPassword($this->user, $domain, $account, $password)); @@ -73,8 +73,8 @@ public function changeAccountPassword(string $domain, string $account, string $p * @param string $domain * @param string $account * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function suspendAccount(string $domain, string $account): bool { return $this->client->send(new SuspendMailAccount($this->user, $domain, $account)); @@ -86,8 +86,8 @@ public function suspendAccount(string $domain, string $account): bool { * @param string $domain * @param string $account * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function unsuspendAccount(string $domain, string $account): bool { return $this->client->send(new UnsuspendMailAccount($this->user, $domain, $account)); @@ -99,8 +99,8 @@ public function unsuspendAccount(string $domain, string $account): bool { * @param string $domain * @param string $account * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteAccount(string $domain, string $account): bool { return $this->client->send(new DeleteMailAccount($this->user, $domain, $account)); @@ -114,8 +114,8 @@ public function deleteAccount(string $domain, string $account): bool { * @param bool $antivirus * @param bool $dkim * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function addDomain(string $domain, bool $antispam = true, bool $antivirus = true, bool $dkim = true): bool { return $this->client->send(new MailDomain($this->user, $domain, $antispam, $antivirus, $dkim)); @@ -126,8 +126,8 @@ public function addDomain(string $domain, bool $antispam = true, bool $antivirus * * @param string $domain * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function suspendDomain(string $domain): bool { return $this->client->send(new SuspendMailDomain($this->user, $domain)); @@ -138,8 +138,8 @@ public function suspendDomain(string $domain): bool { * * @param string $domain * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function unsuspendDomain(string $domain): bool { return $this->client->send(new UnsuspendMailDomain($this->user, $domain)); @@ -150,8 +150,8 @@ public function unsuspendDomain(string $domain): bool { * * @param string $domain * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteDomain(string $domain): bool { return $this->client->send(new DeleteMailDomain($this->user, $domain)); @@ -162,8 +162,8 @@ public function deleteDomain(string $domain): bool { * * @param string $domain * @return array[] - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listDomainDkim(string $domain): array { return $this->client->send(new MailDomainDkim($this->user, $domain)); @@ -174,8 +174,8 @@ public function listDomainDkim(string $domain): array { * * @param string $domain * @return array[] - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listDomainDkimDns(string $domain): array { return $this->client->send(new MailDomainDkimDns($this->user, $domain)); @@ -185,8 +185,8 @@ public function listDomainDkimDns(string $domain): array { * This function of obtaining the list of all user domains. * * @return array[] - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listDomains(): array { return $this->client->send(new MailDomains($this->user)); diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index aefa786..bfd5516 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -17,8 +17,8 @@ class Users extends Module { * This function to obtain the list of all system users. * * @return array - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function list(): array { return $this->client->send(new ListsUsers); @@ -29,8 +29,8 @@ public function list(): array { * * @param string $user * @return array - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function detail(string $user): array { return $this->client->send(new User($user)); @@ -42,8 +42,8 @@ public function detail(string $user): array { * @param string $user * @param string $password * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function changePassword(string $user, string $password): bool { return $this->client->send(new UserPassword($user, $password)); @@ -57,10 +57,10 @@ public function changePassword(string $user, string $password): bool { * @param string $email * @param string|null $package * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ - public function add(string $user, string $password, string $email, string $package = null): bool { + public function add(string $user, string $password, string $email, ?string $package = null): bool { return $this->client->send(new AddUser($user, $password, $email, $package)); } @@ -70,8 +70,8 @@ public function add(string $user, string $password, string $email, string $packa * @param string $user * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function suspend(string $user, bool $restart = false): bool { return $this->client->send(new SuspendUser($user, $restart)); @@ -83,8 +83,8 @@ public function suspend(string $user, bool $restart = false): bool { * @param string $user * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function unsuspend(string $user, bool $restart = false): bool { return $this->client->send(new UnsuspendUser($user, $restart)); @@ -95,8 +95,8 @@ public function unsuspend(string $user, bool $restart = false): bool { * * @param string $user * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function delete(string $user): bool { return $this->client->send(new DeleteUser($user)); @@ -106,8 +106,8 @@ public function delete(string $user): bool { * This function backups all system users. * * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function backup(): bool { return $this->client->send(new BackupUsers); diff --git a/src/HestiaCP/Module/Webs.php b/src/HestiaCP/Module/Webs.php index b3b632d..2e1b47f 100644 --- a/src/HestiaCP/Module/Webs.php +++ b/src/HestiaCP/Module/Webs.php @@ -34,10 +34,10 @@ public function __construct(Client $client, string $user) { * @param string|null $proxyExtensions * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ - public function addDomain(string $domain, string $ip = null, string $aliases = null, string $proxyExtensions = null, bool $restart = false): bool { + public function addDomain(string $domain, ?string $ip = null, ?string $aliases = null, ?string $proxyExtensions = null, bool $restart = false): bool { return $this->client->send(new WebDomain($this->user, $domain, $ip, $aliases, $proxyExtensions, $restart)); } @@ -47,8 +47,8 @@ public function addDomain(string $domain, string $ip = null, string $aliases = n * @param string $domain * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function suspendDomain(string $domain, bool $restart = false): bool { return $this->client->send(new SuspendWebDomain($this->user, $domain, $restart)); @@ -60,8 +60,8 @@ public function suspendDomain(string $domain, bool $restart = false): bool { * @param string $domain * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function unsuspendDomain(string $domain, bool $restart = false): bool { return $this->client->send(new UnsuspendWebDomain($this->user, $domain, $restart)); @@ -72,8 +72,8 @@ public function unsuspendDomain(string $domain, bool $restart = false): bool { * * @param string $domain * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteDomain(string $domain): bool { return $this->client->send(new DeleteWebDomain($this->user, $domain)); @@ -86,10 +86,10 @@ public function deleteDomain(string $domain): bool { * @param string|null $aliases * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ - public function addDomainLetsEncrypt(string $domain, string $aliases = null, bool $restart = false): bool { + public function addDomainLetsEncrypt(string $domain, ?string $aliases = null, bool $restart = false): bool { return $this->client->send(new LetsEncryptDomain($this->user, $domain, $aliases, $restart)); } @@ -99,8 +99,8 @@ public function addDomainLetsEncrypt(string $domain, string $aliases = null, boo * @param string $domain * @param bool $restart * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteDomainLetsEncrypt(string $domain, bool $restart = false): bool { return $this->client->send(new DeleteLetsEncryptDomain($this->user, $domain, $restart)); @@ -114,10 +114,10 @@ public function deleteDomainLetsEncrypt(string $domain, bool $restart = false): * @param string $ftpPassword * @param string|null $ftpPath * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ - public function addDomainFtp(string $domain, string $ftpUser, string $ftpPassword, string $ftpPath = null): bool { + public function addDomainFtp(string $domain, string $ftpUser, string $ftpPassword, ?string $ftpPath = null): bool { return $this->client->send(new WebDomainFtp($this->user, $domain, $ftpUser, $ftpPassword, $ftpPath)); } @@ -128,8 +128,8 @@ public function addDomainFtp(string $domain, string $ftpUser, string $ftpPasswor * @param string $ftpUser * @param string $ftpPassword * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function changeDomainFtpPassword(string $domain, string $ftpUser, string $ftpPassword): bool { return $this->client->send(new WebDomainFtpPassword($this->user, $domain, $ftpUser, $ftpPassword)); @@ -142,8 +142,8 @@ public function changeDomainFtpPassword(string $domain, string $ftpUser, string * @param string $ftpUser * @param string $ftpPath * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function changeDomainFtpPath(string $domain, string $ftpUser, string $ftpPath): bool { return $this->client->send(new WebDomainFtpPath($this->user, $domain, $ftpUser, $ftpPath)); @@ -155,8 +155,8 @@ public function changeDomainFtpPath(string $domain, string $ftpUser, string $ftp * @param string $domain * @param string $ftpUser * @return bool - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function deleteDomainFtp(string $domain, string $ftpUser): bool { return $this->client->send(new DeleteWebDomainFtp($this->user, $domain, $ftpUser)); @@ -166,8 +166,8 @@ public function deleteDomainFtp(string $domain, string $ftpUser): bool { * This function to obtain the list of all user web domains. * * @return array[] - * @throws \neto737\HestiaCP\ClientException - * @throws \neto737\HestiaCP\ProcessException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function listDomains(): array { return $this->client->send(new WebDomains($this->user)); diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index dac8258..37826c0 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -6,17 +6,17 @@ use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use neto737\HestiaCP\AuthorizationException; +use neto737\HestiaCP\Exceptions\AuthorizationException; use neto737\HestiaCP\Client; -use neto737\HestiaCP\ClientException; +use neto737\HestiaCP\Exceptions\ClientException; use neto737\HestiaCP\Command\Add\LetsEncryptDomain; use neto737\HestiaCP\Command\Add\User as AddUser; use neto737\HestiaCP\Command\Lists\Databases; use neto737\HestiaCP\Command\Lists\User; use neto737\HestiaCP\Command\Lists\Users; use neto737\HestiaCP\Command\ResponseCode; -use neto737\HestiaCP\InvalidResponseException; -use neto737\HestiaCP\ProcessException; +use neto737\HestiaCP\Exceptions\InvalidResponseException; +use neto737\HestiaCP\Exceptions\ProcessException; require_once __DIR__ . '/../vendor/autoload.php'; diff --git a/tests/commandAddTest.php b/tests/commandAddTest.php index 9b3c561..e5ded21 100644 --- a/tests/commandAddTest.php +++ b/tests/commandAddTest.php @@ -19,7 +19,7 @@ public function testUser(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -37,7 +37,7 @@ public function testWebDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -54,7 +54,7 @@ public function testWebDomainFtp(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -70,7 +70,7 @@ public function testLEDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -86,7 +86,7 @@ public function testMailAccount(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -103,7 +103,7 @@ public function testMailDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -120,7 +120,7 @@ public function testDatabase(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } } diff --git a/tests/commandBackupTest.php b/tests/commandBackupTest.php index 37615af..134b500 100644 --- a/tests/commandBackupTest.php +++ b/tests/commandBackupTest.php @@ -12,7 +12,7 @@ public function testBackupUsers(): void { $this->assertSame('v-backup-users', $command->getName()); $this->assertSame([], $command->getRequestParams()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -21,7 +21,7 @@ public function testBackupUser(): void { $this->assertSame('v-backup-user', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'yes'], $command->getRequestParams()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } } diff --git a/tests/commandChangeTest.php b/tests/commandChangeTest.php index 4a9fe02..e88b5a9 100644 --- a/tests/commandChangeTest.php +++ b/tests/commandChangeTest.php @@ -16,7 +16,7 @@ public function testUserPassword(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -32,7 +32,7 @@ public function testMailAccountPassword(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -48,7 +48,7 @@ public function testWebDomainFtpPassword(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -64,7 +64,7 @@ public function testWebDomainFtpPath(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } } diff --git a/tests/commandDeleteTest.php b/tests/commandDeleteTest.php index b0ab7d3..9354c4e 100644 --- a/tests/commandDeleteTest.php +++ b/tests/commandDeleteTest.php @@ -22,7 +22,7 @@ public function testUser(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -36,7 +36,7 @@ public function testWebDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -51,7 +51,7 @@ public function testWebDomainFtp(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -66,7 +66,7 @@ public function testLEDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -81,7 +81,7 @@ public function testMailAccount(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -95,7 +95,7 @@ public function testMailDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -109,7 +109,7 @@ public function testUserBackup(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -122,7 +122,7 @@ public function testUserBackupExclusions(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -136,7 +136,7 @@ public function testDatabase(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -149,7 +149,7 @@ public function testDatabases(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } } diff --git a/tests/commandListTest.php b/tests/commandListTest.php index e95fe41..7417ba4 100644 --- a/tests/commandListTest.php +++ b/tests/commandListTest.php @@ -24,7 +24,7 @@ public function testUser(): void { $this->assertFalse($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -87,7 +87,7 @@ public function testUserBackup(): void { $this->assertSame('v-list-user-backup', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'admin.2021-10-07_05-15-09.tar', 'arg3' => 'json'], $command->getRequestParams()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -104,7 +104,7 @@ public function testListDatabase(): void { $this->assertSame('v-list-database', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'test', 'arg3' => 'json'], $command->getRequestParams()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } diff --git a/tests/commandSuspendTest.php b/tests/commandSuspendTest.php index 5ff7c66..19540d9 100644 --- a/tests/commandSuspendTest.php +++ b/tests/commandSuspendTest.php @@ -16,7 +16,7 @@ public function testUser(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -31,7 +31,7 @@ public function testWebDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -46,7 +46,7 @@ public function testMailAccount(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -60,7 +60,7 @@ public function testMailDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } } diff --git a/tests/commandTest.php b/tests/commandTest.php index f3b0724..57fc689 100644 --- a/tests/commandTest.php +++ b/tests/commandTest.php @@ -1,7 +1,7 @@ assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -31,7 +31,7 @@ public function testWebDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -46,7 +46,7 @@ public function testMailAccount(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } @@ -60,7 +60,7 @@ public function testMailDomain(): void { $this->assertTrue($command->needReturnCode()); - $this->expectException(\neto737\HestiaCP\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } } From d9492ce2a33b2cfa8bec1d123457e70b045f8738 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:52:17 +1030 Subject: [PATCH 05/19] - Updated phpunit library - Added support to access key + secret key auth --- README.md | 13 +++-- composer.json | 2 +- src/HestiaCP/Authorization/Credentials.php | 55 +++++++++++++++++++++- tests/clientSendTest.php | 23 +++++++++ tests/credentialTest.php | 29 ++++++++++++ 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 35019da..75da247 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,16 @@ use neto737\HestiaCP\Client; use neto737\HestiaCP\Authorization\Credentials; use neto737\HestiaCP\Authorization\Host; -// You can choose to use an API Key or username and password +// You can choose to use an access key, a legacy API key, or username and password -// API Key -$credentials = new Credentials('API_Key'); +// Access key (HestiaCP 1.6+) +$credentials = Credentials::accessKey('ACCESS_KEY', 'SECRET_KEY'); -// Username and Password -$credentials = new Credentials('someUser', 'somePassword'); +// Legacy API Key +$credentials = Credentials::legacyHash('API_Key'); + +// Username and Password (HestiaCP only accepts the admin/root user) +$credentials = Credentials::password('admin', 'somePassword'); $host = new Host('https://someHost', $credentials); diff --git a/composer.json b/composer.json index ffb17ab..750a29f 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^9" + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0" } } diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php index 94661aa..7bcda55 100644 --- a/src/HestiaCP/Authorization/Credentials.php +++ b/src/HestiaCP/Authorization/Credentials.php @@ -13,9 +13,15 @@ class Credentials { /** @var string */ private $api_key; + /** @var string */ + private $access_key; + + /** @var string */ + private $secret_key; + /** * @param string $user Username or Api Key - * @param string @password Password (must be null if using API Key) + * @param string $password Password (must be null if using API Key) */ public function __construct(string $user, ?string $password = null) { if (is_null($password)) { @@ -26,8 +32,46 @@ public function __construct(string $user, ?string $password = null) { } } + /** + * Access key authentication (HestiaCP 1.6+) + */ + public static function accessKey(string $accessKey, string $secretKey): self { + return (new self(''))->setAccessKey($accessKey, $secretKey); + } + + /** + * Legacy API key, sent as `hash` + */ + public static function legacyHash(string $hash): self { + return new self($hash); + } + + /** + * Username and password (HestiaCP only accepts the admin/root user) + */ + public static function password(string $user, string $password): self { + return new self($user, $password); + } + + public function setAccessKey(string $accessKey, string $secretKey) { + $this->access_key = $accessKey; + $this->secret_key = $secretKey; + $this->api_key = null; + return $this; + } + + public function getAccessKey(): ?string { + return $this->access_key; + } + + public function getSecretKey(): ?string { + return $this->secret_key; + } + public function setApiKey(string $key) { $this->api_key = $key; + $this->access_key = null; + $this->secret_key = null; return $this; } @@ -54,12 +98,19 @@ public function getPassword(): ?string { } public function getRequestParams(): array { + if ($this->access_key !== null) { + return [ + 'access_key' => $this->getAccessKey(), + 'secret_key' => $this->getSecretKey() + ]; + } + if ($this->api_key !== null) { return [ 'hash' => $this->getApiKey() ]; } - + return [ 'user' => $this->getUser(), 'password' => $this->getPassword() diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index 37826c0..0248fef 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -6,6 +6,8 @@ use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use neto737\HestiaCP\Authorization\Credentials; +use neto737\HestiaCP\Authorization\Host; use neto737\HestiaCP\Exceptions\AuthorizationException; use neto737\HestiaCP\Client; use neto737\HestiaCP\Exceptions\ClientException; @@ -60,6 +62,27 @@ public function testRequestFormat(): void { ], $params); } + public function testRequestFormatWithAccessKey(): void { + $this->history = []; + $stack = HandlerStack::create(new MockHandler([self::codeResponse(0)])); + $stack->push(Middleware::history($this->history)); + + $host = new Host('https://someHost', Credentials::accessKey('ACCESS', 'SECRET')); + $client = new Client($host, ['handler' => $stack]); + $client->send(new AddUser('user', 'pass', 'mail@domain.com')); + + parse_str((string) $this->history[0]['request']->getBody(), $params); + $this->assertSame([ + 'access_key' => 'ACCESS', + 'secret_key' => 'SECRET', + 'arg1' => 'user', + 'arg2' => 'pass', + 'arg3' => 'mail@domain.com', + 'returncode' => 'yes', + 'cmd' => 'v-add-user' + ], $params); + } + public function testVerifyTlsByDefault(): void { $client = $this->createClient([self::codeResponse(0)]); $client->send(new AddUser('user', 'pass', 'mail@domain.com')); diff --git a/tests/credentialTest.php b/tests/credentialTest.php index a59cb2e..bfd4a64 100644 --- a/tests/credentialTest.php +++ b/tests/credentialTest.php @@ -36,5 +36,34 @@ public function testParams() { $credentials_apikey = new Credentials('key'); $this->assertSame(['hash' => 'key'], $credentials_apikey->getRequestParams()); + + $credentials_accesskey = Credentials::accessKey('access', 'secret'); + $this->assertSame(['access_key' => 'access', 'secret_key' => 'secret'], $credentials_accesskey->getRequestParams()); + } + + public function testAccessKey() { + $credentials = Credentials::accessKey('access', 'secret'); + $this->assertSame('access', $credentials->getAccessKey()); + $this->assertSame('secret', $credentials->getSecretKey()); + $this->assertNull($credentials->getApiKey()); + $this->assertNull($credentials->getUser()); + $this->assertNull($credentials->getPassword()); + } + + public function testSwitchBetweenKeys() { + $credentials = Credentials::accessKey('access', 'secret'); + $credentials->setApiKey('key'); + $this->assertNull($credentials->getAccessKey()); + $this->assertNull($credentials->getSecretKey()); + $this->assertSame(['hash' => 'key'], $credentials->getRequestParams()); + + $credentials->setAccessKey('access', 'secret'); + $this->assertNull($credentials->getApiKey()); + $this->assertSame(['access_key' => 'access', 'secret_key' => 'secret'], $credentials->getRequestParams()); + } + + public function testNamedConstructors() { + $this->assertSame(['hash' => 'key'], Credentials::legacyHash('key')->getRequestParams()); + $this->assertSame(['user' => 'us', 'password' => 'er'], Credentials::password('us', 'er')->getRequestParams()); } } From e860ad0a4571323b1f82d1724f176a4fdb95d150 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:56:23 +1030 Subject: [PATCH 06/19] Hostname validation and normalisation --- README.md | 2 + src/HestiaCP/Authorization/Host.php | 49 ++++++++++++++++++- src/HestiaCP/Client.php | 4 +- tests/clientTest.php | 16 +++--- tests/hostTest.php | 76 ++++++++++++++++++++++++++--- tests/moduleTest.php | 2 +- 6 files changed, 132 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 75da247..f11c4be 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ $client = Client::simpleFactory('https://someHost', 'admin', 'somePass'); ``` +The hostname must start with `https://` (or `http://`) and contain only the host, for example `https://panel.example.com`. An IP address works too: `https://192.0.2.10`, or `https://[2001:db8::1]` for IPv6 (the brackets are required). A trailing slash is removed. The port is a separate argument of `Host` (default `8083`). An invalid hostname throws `\neto737\HestiaCP\Exceptions\InvalidArgumentException`. + The API has to be enabled in the HestiaCP server settings, and the IP address of the machine that calls it has to be allowed. Client options can be passed as the last argument of `simpleFactory()` or `new Client()`: diff --git a/src/HestiaCP/Authorization/Host.php b/src/HestiaCP/Authorization/Host.php index f636b0c..4e4d5b3 100644 --- a/src/HestiaCP/Authorization/Host.php +++ b/src/HestiaCP/Authorization/Host.php @@ -2,6 +2,8 @@ namespace neto737\HestiaCP\Authorization; +use neto737\HestiaCP\Exceptions\InvalidArgumentException; + class Host { /** @var string */ @@ -13,14 +15,26 @@ class Host { /** @var int */ private $port; + /** + * @param string $hostname Scheme and host, e.g. https://panel.example.com + * @param Credentials $credentials + * @param int $port + * @throws InvalidArgumentException + */ public function __construct(string $hostname, Credentials $credentials, int $port = 8083) { $this->setHostname($hostname); $this->setCredentials($credentials); $this->setPort($port); } + /** + * The hostname must start with http:// or https://. A trailing slash is removed + * and the scheme and host are lowercased. The port is set separately (see setPort()). + * + * @throws InvalidArgumentException + */ public function setHostname(string $hostname) { - $this->hostname = $hostname; + $this->hostname = self::normalizeHostname($hostname); return $this; } @@ -49,4 +63,37 @@ public function getPort(): int { public function buildUri(): string { return $this->getHostname() . ':' . $this->getPort() . '/api/'; } + + /** + * @throws InvalidArgumentException + */ + private static function normalizeHostname(string $hostname): string { + $hostname = trim($hostname); + + $authority = preg_replace('~^[a-z][a-z0-9+.-]*://~i', '', $hostname); + if (filter_var(rtrim($authority, '/'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { + throw new InvalidArgumentException("Invalid hostname '$hostname', an IPv6 address must be in brackets, e.g. 'https://[2001:db8::1]'"); + } + + $parts = parse_url($hostname); + + if ($parts === false || !isset($parts['scheme'], $parts['host']) || $parts['host'] === '') { + throw new InvalidArgumentException("Invalid hostname '$hostname', expected e.g. 'https://panel.example.com'"); + } + + $scheme = strtolower($parts['scheme']); + if ($scheme !== 'http' && $scheme !== 'https') { + throw new InvalidArgumentException("Invalid hostname '$hostname', the scheme must be http or https"); + } + + if (isset($parts['port'])) { + throw new InvalidArgumentException("Invalid hostname '$hostname', set the port with the port argument instead"); + } + + if (isset($parts['user']) || isset($parts['pass']) || isset($parts['query']) || isset($parts['fragment']) || trim($parts['path'] ?? '', '/') !== '') { + throw new InvalidArgumentException("Invalid hostname '$hostname', it must contain only the scheme and host"); + } + + return $scheme . '://' . strtolower($parts['host']); + } } diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index 8962449..d489c0c 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -8,6 +8,7 @@ use neto737\HestiaCP\Command\ICommand; use neto737\HestiaCP\Command\TestAuthorization; use neto737\HestiaCP\Exceptions\ClientException; +use neto737\HestiaCP\Exceptions\InvalidArgumentException; use neto737\HestiaCP\Exceptions\ProcessException; use neto737\HestiaCP\Module\Backups; use neto737\HestiaCP\Module\Databases; @@ -45,12 +46,13 @@ public function __construct(Host $host, ?array $options = null) { } /** - * @param string $hostname Hostname + * @param string $hostname Scheme and host, e.g. https://panel.example.com * @param string $user Username or API Key * @param mixed $password Password (must be null if using API Key) * @param array|null $options See Client::__construct() * @return Client * @throws ClientException + * @throws InvalidArgumentException */ public static function simpleFactory(string $hostname, string $user, ?string $password = null, ?array $options = null): Client { $credentials = new Credentials($user, $password); diff --git a/tests/clientTest.php b/tests/clientTest.php index fc37177..eb40047 100644 --- a/tests/clientTest.php +++ b/tests/clientTest.php @@ -10,12 +10,12 @@ class clientTest extends \PHPUnit\Framework\TestCase { public function testHostWithAPIKey(): void { $credentials = new Credentials(''); - $host = new Host('', $credentials); + $host = new Host('https://someHost', $credentials); $client = new Client($host); $this->assertSame($host, $client->getHost()); - $otherHost = new Host('otherHost', $credentials); + $otherHost = new Host('https://otherHost', $credentials); $client->setHost($otherHost); $this->assertNotSame($host, $client->getHost()); $this->assertSame($otherHost, $client->getHost()); @@ -23,34 +23,34 @@ public function testHostWithAPIKey(): void { public function testHost(): void { $credentials = new Credentials('', ''); - $host = new Host('', $credentials); + $host = new Host('https://someHost', $credentials); $client = new Client($host); $this->assertSame($host, $client->getHost()); - $otherHost = new Host('otherHost', $credentials); + $otherHost = new Host('https://otherHost', $credentials); $client->setHost($otherHost); $this->assertNotSame($host, $client->getHost()); $this->assertSame($otherHost, $client->getHost()); } public function testSimpleFactoryWithAPIKey(): void { - $client = Client::simpleFactory('someHost', 'ApiKey'); + $client = Client::simpleFactory('https://someHost', 'ApiKey'); $host = $client->getHost(); $credentials = $host->getCredentials(); - $this->assertSame('someHost', $host->getHostname()); + $this->assertSame('https://somehost', $host->getHostname()); $this->assertSame('ApiKey', $credentials->getApiKey()); } public function testSimpleFactory(): void { - $client = Client::simpleFactory('someHost', 'someUser', 'somePass'); + $client = Client::simpleFactory('https://someHost', 'someUser', 'somePass'); $host = $client->getHost(); $credentials = $host->getCredentials(); - $this->assertSame('someHost', $host->getHostname()); + $this->assertSame('https://somehost', $host->getHostname()); $this->assertSame('someUser', $credentials->getUser()); $this->assertSame('somePass', $credentials->getPassword()); } diff --git a/tests/hostTest.php b/tests/hostTest.php index 4c29162..3d1db33 100644 --- a/tests/hostTest.php +++ b/tests/hostTest.php @@ -2,6 +2,7 @@ use neto737\HestiaCP\Authorization\Credentials; use neto737\HestiaCP\Authorization\Host; +use neto737\HestiaCP\Exceptions\InvalidArgumentException; require_once __DIR__ . '/../vendor/autoload.php'; @@ -9,12 +10,72 @@ class hostTest extends \PHPUnit\Framework\TestCase { public function testHost() { $credentials = new Credentials('', ''); - $host = new Host('', $credentials); + $host = new Host('https://hestia', $credentials); + + $this->assertSame('https://hestia', $host->getHostname()); + + $host->setHostname('http://host'); + $this->assertSame('http://host', $host->getHostname()); + } + + public static function normalizedHostnames(): array { + return [ + 'trailing slash' => ['https://hestia/', 'https://hestia'], + 'several trailing slashes' => ['https://hestia///', 'https://hestia'], + 'surrounding whitespace' => [" https://hestia \n", 'https://hestia'], + 'uppercase scheme and host' => ['HTTPS://Panel.Example.COM', 'https://panel.example.com'], + 'ip address' => ['https://192.0.2.10', 'https://192.0.2.10'], + 'ipv6 address' => ['https://[2001:db8::1]/', 'https://[2001:db8::1]'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('normalizedHostnames')] + public function testNormalizesHostname(string $hostname, string $expected) { + $host = new Host($hostname, new Credentials('', '')); + + $this->assertSame($expected, $host->getHostname()); + } + + public static function invalidHostnames(): array { + return [ + 'empty' => [''], + 'missing scheme' => ['hestia'], + 'missing scheme with port' => ['hestia:8083'], + 'scheme only' => ['https://'], + 'unsupported scheme' => ['ftp://hestia'], + 'port' => ['https://hestia:8083'], + 'ip address with port' => ['https://192.0.2.10:8083'], + 'ipv6 address with port' => ['https://[2001:db8::1]:8083'], + 'path' => ['https://hestia/api/'], + 'query' => ['https://hestia?a=b'], + 'fragment' => ['https://hestia#top'], + 'user info' => ['https://user:pass@hestia'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('invalidHostnames')] + public function testRejectsInvalidHostname(string $hostname) { + $this->expectException(InvalidArgumentException::class); + + new Host($hostname, new Credentials('', '')); + } + + public function testRejectsUnbracketedIpv6() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be in brackets'); - $this->assertSame('', $host->getHostname()); + new Host('https://2001:db8::1', new Credentials('', '')); + } + + public function testSetHostnameKeepsPreviousValueOnError() { + $host = new Host('https://hestia', new Credentials('', '')); - $host->setHostname('host'); - $this->assertSame('host', $host->getHostname()); + try { + $host->setHostname('hestia'); + $this->fail('Expected InvalidArgumentException'); + } catch (InvalidArgumentException $e) { + $this->assertSame('https://hestia', $host->getHostname()); + } } public function testUri() { @@ -22,11 +83,14 @@ public function testUri() { $host = new Host('https://hestia', $credentials); $this->assertSame('https://hestia:8083/api/', $host->buildUri()); + + $host = new Host('https://hestia/', $credentials, 2083); + $this->assertSame('https://hestia:2083/api/', $host->buildUri()); } public function testPort() { $credentials = new Credentials('', ''); - $host = new Host('', $credentials); + $host = new Host('https://hestia', $credentials); $this->assertSame(8083, $host->getPort()); @@ -36,7 +100,7 @@ public function testPort() { public function testCredentials() { $credentials = new Credentials('', ''); - $host = new Host('', $credentials); + $host = new Host('https://hestia', $credentials); $this->assertSame($credentials, $host->getCredentials()); diff --git a/tests/moduleTest.php b/tests/moduleTest.php index 4aa9d40..11bc99d 100644 --- a/tests/moduleTest.php +++ b/tests/moduleTest.php @@ -12,7 +12,7 @@ class moduleTest extends \PHPUnit\Framework\TestCase { public function createClient(): Client { - return Client::simpleFactory('someHost', 'someUser', 'somePass'); + return Client::simpleFactory('https://someHost', 'someUser', 'somePass'); } public function testModuleUser(): void { From c5bba4b26c9e8967eb031df531c23097afe7fcbe Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 13:59:38 +1030 Subject: [PATCH 07/19] Updated doc link --- src/HestiaCP/Command/Delete/UserBackupExclusions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HestiaCP/Command/Delete/UserBackupExclusions.php b/src/HestiaCP/Command/Delete/UserBackupExclusions.php index 81e20c6..1dbd1b1 100644 --- a/src/HestiaCP/Command/Delete/UserBackupExclusions.php +++ b/src/HestiaCP/Command/Delete/UserBackupExclusions.php @@ -12,7 +12,7 @@ class UserBackupExclusions extends ProcessCommand { /** * TODO: * Added system parameter as in the HestiaCP docs - * https://docs.hestiacp.com/cli_commands/commands.html#v-delete-user-backup-exclusions + * https://hestiacp.com/docs/introduction/getting-started#v-delete-user-backup-exclusions */ public function __construct(string $user) { From 3683df4042bd3c9021e077d5abd6b33d67ba13a4 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 14:14:02 +1030 Subject: [PATCH 08/19] - Updated Guzzle version - Added GitHub Actions as CI tests --- .github/workflows/tests.yml | 45 +++++++++++++++++++++++++++++++++++++ .gitignore | 4 +++- .travis.yml | 14 ------------ README.md | 17 ++------------ composer.json | 2 +- phpunit.xml.dist | 17 ++++++++++++++ 6 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml create mode 100644 phpunit.xml.dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f0d59d3 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,45 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + phpunit: + name: PHP ${{ matrix.php }} (${{ matrix.dependencies }} deps) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + dependencies: [highest] + include: + - php: '8.1' + dependencies: lowest + + steps: + - uses: actions/checkout@v5 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: pcov + tools: composer:v2 + + - name: Install dependencies + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + composer-options: --prefer-dist + + - name: Run tests + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage + if: matrix.php == '8.4' && matrix.dependencies == 'highest' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 57462ef..b48fa4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ composer.lock test.php /vendor -/tests/log \ No newline at end of file +/tests/log +/.phpunit.cache +coverage.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ecf36ce..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: php - -php: - - 7.3.24 - -before_script: - - travis_retry composer self-update - - travis_retry composer install --no-interaction --no-suggest - -script: - - vendor/bin/phpunit --coverage-clover=coverage.xml --whitelist src ./tests - -after_success: - - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index f11c4be..641a682 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # HestiaCP PHP API -[![Name](https://badgen.net/packagist/name/neto737/hestiacp-api)](//packagist.org/packages/neto737/hestiacp-api) [![Latest Stable Version](https://badgen.net/packagist/v/neto737/hestiacp-api/latest)](https://packagist.org/packages/neto737/hestiacp-api) [![Total Downloads](https://badgen.net/packagist/dt/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![License](https://badgen.net/packagist/license/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![Build Status](https://www.travis-ci.com/neto737/HestiaCP-API.svg?branch=main)](https://www.travis-ci.com/github/neto737/HestiaCP-API) [![codecov](https://codecov.io/gh/neto737/HestiaCP-API/branch/main/graph/badge.svg?token=F93I5JQXY6)](https://codecov.io/gh/neto737/HestiaCP-API) +[![Name](https://badgen.net/packagist/name/neto737/hestiacp-api)](//packagist.org/packages/neto737/hestiacp-api) [![Latest Stable Version](https://badgen.net/packagist/v/neto737/hestiacp-api/latest)](https://packagist.org/packages/neto737/hestiacp-api) [![Total Downloads](https://badgen.net/packagist/dt/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![License](https://badgen.net/packagist/license/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![Tests](https://github.com/neto737/HestiaCP-API/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/neto737/HestiaCP-API/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/neto737/HestiaCP-API/branch/main/graph/badge.svg?token=F93I5JQXY6)](https://codecov.io/gh/neto737/HestiaCP-API) @@ -249,20 +249,7 @@ $dbModule->listDatabases(); ``` -e) cron module - -```php - -$cronModule = $client->getModuleCron(); - - -// todo - -// ... etc - -``` - -f) backup module +e) backup module ```php diff --git a/composer.json b/composer.json index 750a29f..0c39f49 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "require": { "php": ">=8.1", "ext-json": "*", - "guzzlehttp/guzzle": "^7.4" + "guzzlehttp/guzzle": "^7.15.2 || ^8.2" }, "homepage": "https://github.com/neto737/HestiaCP-API", "license": "MIT", diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..4bde472 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + + + tests + + + + + src + + + From 7b58ac8caf0137c2d9f5074631bb3d34148c9d84 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 14:29:43 +1030 Subject: [PATCH 09/19] Bugfixes in some commands --- README.md | 8 +++ .../Command/Add/LetsEncryptDomain.php | 40 +++++++++++++-- src/HestiaCP/Command/Add/User.php | 27 ++++++++-- .../Command/Delete/UserBackupExclusions.php | 20 +++++--- src/HestiaCP/Module/Backups.php | 7 +-- src/HestiaCP/Module/Users.php | 6 ++- src/HestiaCP/Module/Webs.php | 8 +-- tests/clientSendTest.php | 2 + tests/commandAddTest.php | 49 ++++++++++++++++++- tests/commandDeleteTest.php | 5 ++ 10 files changed, 148 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 641a682..261feb8 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ $userModule->changePassword('admin', 'otherPa$$word'); $userModule->add('other_user', 'pa$$word', 'some@email.com'); +$userModule->add('other_user', 'pa$$word', 'some@email.com', 'default', 'John', 'Doe'); // with package, first name and last name + $userModule->suspend('other_user'); $userModule->unsuspend('other_user'); @@ -172,6 +174,10 @@ $webModule->addDomain('domain.com'); $webModule->addDomainLetsEncrypt('domain.com', 'www.domain.com'); // needs longer timeout +$webModule->addDomainLetsEncrypt('domain.com', null, true); // certificate for the mail domain + +$webModule->addDomainLetsEncrypt('domain.com', 'www.domain.com', false, 'rsa'); // key algorithm: ecdsa-256 (default), ecdsa-384 or rsa (needs HestiaCP newer than 1.10.5) + $webModule->deleteDomainLetsEncrypt('domain.com'); $webModule->addDomainFtp('domain.com', 'test', 'pa$$word'); @@ -261,6 +267,8 @@ $backupModule->delete('admin.2021-10-13_18-12-53.tar'); // delete an user backup $backupModule->deleteExclusions(); // delete all backup exclusions +$backupModule->deleteExclusions('WEB'); // delete only one type of exclusion (WEB, DNS, MAIL, DB, CRON or USER) + $backupModule->listBackups(); // returns the backups list $backupModule->listBackup('admin.2021-10-13_18-12-53.tar'); // returns backup parameters list diff --git a/src/HestiaCP/Command/Add/LetsEncryptDomain.php b/src/HestiaCP/Command/Add/LetsEncryptDomain.php index e082a59..dd1e73f 100644 --- a/src/HestiaCP/Command/Add/LetsEncryptDomain.php +++ b/src/HestiaCP/Command/Add/LetsEncryptDomain.php @@ -3,9 +3,16 @@ namespace neto737\HestiaCP\Command\Add; use neto737\HestiaCP\Command\ProcessCommand; +use neto737\HestiaCP\Exceptions\InvalidArgumentException; class LetsEncryptDomain extends ProcessCommand { + public const KEY_ALGO_ECDSA_256 = 'ecdsa-256'; + public const KEY_ALGO_ECDSA_384 = 'ecdsa-384'; + public const KEY_ALGO_RSA = 'rsa'; + + private const KEY_ALGOS = [self::KEY_ALGO_ECDSA_256, self::KEY_ALGO_ECDSA_384, self::KEY_ALGO_RSA]; + /** @var string */ private $user; @@ -16,13 +23,30 @@ class LetsEncryptDomain extends ProcessCommand { private $aliases; /** @var bool */ - private $restart; + private $mail; + + /** @var string|null */ + private $keyAlgo; + + /** + * @param string $user + * @param string $domain + * @param string|null $aliases + * @param bool $mail Issue the certificate for the mail domain instead of the web domain + * @param string|null $keyAlgo One of the KEY_ALGO_* constants. HestiaCP uses ecdsa-256 when it isn't set. + * Needs a HestiaCP version newer than 1.10.5. + * @throws InvalidArgumentException + */ + public function __construct(string $user, string $domain, ?string $aliases = null, bool $mail = false, ?string $keyAlgo = null) { + if ($keyAlgo !== null && !in_array($keyAlgo, self::KEY_ALGOS, true)) { + throw new InvalidArgumentException("Invalid key algorithm '$keyAlgo', expected one of: " . implode(', ', self::KEY_ALGOS)); + } - public function __construct(string $user, string $domain, ?string $aliases = null, bool $restart = false) { $this->user = $user; $this->domain = $domain; $this->aliases = $aliases; - $this->restart = $restart; + $this->mail = $mail; + $this->keyAlgo = $keyAlgo; } public function getName(): string { @@ -34,11 +58,17 @@ public function getTimeout(): ?float { } public function getRequestParams(): array { - return [ + $params = [ self::ARG_1 => $this->user, self::ARG_2 => $this->domain, self::ARG_3 => $this->aliases ?: '', - self::ARG_4 => $this->convertBool($this->restart), + self::ARG_4 => $this->convertBool($this->mail), ]; + + if ($this->keyAlgo !== null) { + $params[self::ARG_5] = $this->keyAlgo; + } + + return $params; } } diff --git a/src/HestiaCP/Command/Add/User.php b/src/HestiaCP/Command/Add/User.php index b852a9d..b19ec25 100644 --- a/src/HestiaCP/Command/Add/User.php +++ b/src/HestiaCP/Command/Add/User.php @@ -15,14 +15,22 @@ class User extends ProcessCommand { /** @var string */ private $email; - /** @var string */ + /** @var string|null */ private $package; - public function __construct(string $user, string $password, string $email, ?string $package = null) { + /** @var string|null */ + private $name; + + /** @var string|null */ + private $lastname; + + public function __construct(string $user, string $password, string $email, ?string $package = null, ?string $name = null, ?string $lastname = null) { $this->user = $user; $this->password = $password; $this->email = $email; $this->package = $package; + $this->name = $name; + $this->lastname = $lastname; } public function getName(): string { @@ -30,11 +38,22 @@ public function getName(): string { } public function getRequestParams(): array { - return [ + // HestiaCP passes the arguments by position, so every argument before the last one set has to be sent + $params = [ self::ARG_1 => $this->user, self::ARG_2 => $this->password, self::ARG_3 => $this->email, - self::ARG_4 => $this->package + self::ARG_4 => $this->package ?? 'default' ]; + + if ($this->name !== null || $this->lastname !== null) { + $params[self::ARG_5] = $this->name ?? ''; + } + + if ($this->lastname !== null) { + $params[self::ARG_6] = $this->lastname; + } + + return $params; } } diff --git a/src/HestiaCP/Command/Delete/UserBackupExclusions.php b/src/HestiaCP/Command/Delete/UserBackupExclusions.php index 1dbd1b1..5942b84 100644 --- a/src/HestiaCP/Command/Delete/UserBackupExclusions.php +++ b/src/HestiaCP/Command/Delete/UserBackupExclusions.php @@ -9,14 +9,16 @@ class UserBackupExclusions extends ProcessCommand { /** @var string */ private $user; + /** @var string|null */ + private $system; + /** - * TODO: - * Added system parameter as in the HestiaCP docs - * https://hestiacp.com/docs/introduction/getting-started#v-delete-user-backup-exclusions + * @param string $user + * @param string|null $system Exclusion type to delete (WEB, DNS, MAIL, DB, CRON or USER). Deletes all of them when not set. */ - - public function __construct(string $user) { + public function __construct(string $user, ?string $system = null) { $this->user = $user; + $this->system = $system; } public function getName(): string { @@ -24,8 +26,14 @@ public function getName(): string { } public function getRequestParams(): array { - return [ + $params = [ self::ARG_1 => $this->user ]; + + if ($this->system !== null) { + $params[self::ARG_2] = $this->system; + } + + return $params; } } diff --git a/src/HestiaCP/Module/Backups.php b/src/HestiaCP/Module/Backups.php index 111ff4f..fd95e26 100644 --- a/src/HestiaCP/Module/Backups.php +++ b/src/HestiaCP/Module/Backups.php @@ -47,13 +47,14 @@ public function delete(string $backup): bool { /** * This function for deleting backup exclusion - * + * + * @param string|null $system Exclusion type to delete (WEB, DNS, MAIL, DB, CRON or USER). Deletes all of them when not set. * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ - public function deleteExclusions(): bool { - return $this->client->send(new DeleteUserBackupExclusions($this->user)); + public function deleteExclusions(?string $system = null): bool { + return $this->client->send(new DeleteUserBackupExclusions($this->user, $system)); } /** diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index bfd5516..135bcfc 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -56,12 +56,14 @@ public function changePassword(string $user, string $password): bool { * @param string $password * @param string $email * @param string|null $package + * @param string|null $name + * @param string|null $lastname * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ - public function add(string $user, string $password, string $email, ?string $package = null): bool { - return $this->client->send(new AddUser($user, $password, $email, $package)); + public function add(string $user, string $password, string $email, ?string $package = null, ?string $name = null, ?string $lastname = null): bool { + return $this->client->send(new AddUser($user, $password, $email, $package, $name, $lastname)); } /** diff --git a/src/HestiaCP/Module/Webs.php b/src/HestiaCP/Module/Webs.php index 2e1b47f..cbd0422 100644 --- a/src/HestiaCP/Module/Webs.php +++ b/src/HestiaCP/Module/Webs.php @@ -84,13 +84,15 @@ public function deleteDomain(string $domain): bool { * * @param string $domain * @param string|null $aliases - * @param bool $restart + * @param bool $mail Issue the certificate for the mail domain instead of the web domain + * @param string|null $keyAlgo One of the LetsEncryptDomain::KEY_ALGO_* constants. Needs a HestiaCP version newer than 1.10.5. * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException + * @throws \neto737\HestiaCP\Exceptions\InvalidArgumentException */ - public function addDomainLetsEncrypt(string $domain, ?string $aliases = null, bool $restart = false): bool { - return $this->client->send(new LetsEncryptDomain($this->user, $domain, $aliases, $restart)); + public function addDomainLetsEncrypt(string $domain, ?string $aliases = null, bool $mail = false, ?string $keyAlgo = null): bool { + return $this->client->send(new LetsEncryptDomain($this->user, $domain, $aliases, $mail, $keyAlgo)); } /** diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index 0248fef..0f774da 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -57,6 +57,7 @@ public function testRequestFormat(): void { 'arg1' => 'user', 'arg2' => 'pass', 'arg3' => 'mail@domain.com', + 'arg4' => 'default', 'returncode' => 'yes', 'cmd' => 'v-add-user' ], $params); @@ -78,6 +79,7 @@ public function testRequestFormatWithAccessKey(): void { 'arg1' => 'user', 'arg2' => 'pass', 'arg3' => 'mail@domain.com', + 'arg4' => 'default', 'returncode' => 'yes', 'cmd' => 'v-add-user' ], $params); diff --git a/tests/commandAddTest.php b/tests/commandAddTest.php index e5ded21..581c5d5 100644 --- a/tests/commandAddTest.php +++ b/tests/commandAddTest.php @@ -15,7 +15,7 @@ class commandAddTest extends \PHPUnit\Framework\TestCase { public function testUser(): void { $command = new User('admin', 'pass', 'mail@domain.com'); $this->assertSame('v-add-user', $command->getName()); - $this->assertSame(['arg1' => 'admin', 'arg2' => 'pass', 'arg3' => 'mail@domain.com', 'arg4' => null], $command->getRequestParams()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'pass', 'arg3' => 'mail@domain.com', 'arg4' => 'default'], $command->getRequestParams()); $this->assertTrue($command->needReturnCode()); @@ -23,6 +23,37 @@ public function testUser(): void { $command->process(); } + public function testUserWithName(): void { + $command = new User('admin', 'pass', 'mail@domain.com', null, 'John', 'Doe'); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'pass', + 'arg3' => 'mail@domain.com', + 'arg4' => 'default', + 'arg5' => 'John', + 'arg6' => 'Doe' + ], $command->getRequestParams()); + + $command = new User('admin', 'pass', 'mail@domain.com', 'custom', 'John'); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'pass', + 'arg3' => 'mail@domain.com', + 'arg4' => 'custom', + 'arg5' => 'John' + ], $command->getRequestParams()); + + $command = new User('admin', 'pass', 'mail@domain.com', null, null, 'Doe'); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'pass', + 'arg3' => 'mail@domain.com', + 'arg4' => 'default', + 'arg5' => '', + 'arg6' => 'Doe' + ], $command->getRequestParams()); + } + public function testWebDomain(): void { $command = new WebDomain('admin', 'domain.com'); $this->assertSame('v-add-web-domain', $command->getName()); @@ -74,6 +105,22 @@ public function testLEDomain(): void { $command->process(); } + public function testLEDomainMailAndKeyAlgo(): void { + $command = new LetsEncryptDomain('admin', 'domain.com', 'www.domain.com', true, LetsEncryptDomain::KEY_ALGO_RSA); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'domain.com', + 'arg3' => 'www.domain.com', + 'arg4' => 'yes', + 'arg5' => 'rsa' + ], $command->getRequestParams()); + } + + public function testLEDomainInvalidKeyAlgo(): void { + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidArgumentException::class); + new LetsEncryptDomain('admin', 'domain.com', null, false, 'dsa'); + } + public function testMailAccount(): void { $command = new MailAccount('admin', 'domain.com', 'account', 'pass'); $this->assertSame('v-add-mail-account', $command->getName()); diff --git a/tests/commandDeleteTest.php b/tests/commandDeleteTest.php index 9354c4e..7ffba1b 100644 --- a/tests/commandDeleteTest.php +++ b/tests/commandDeleteTest.php @@ -120,6 +120,11 @@ public function testUserBackupExclusions(): void { 'arg1' => 'admin' ], $command->getRequestParams()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'WEB' + ], (new UserBackupExclusions('admin', 'WEB'))->getRequestParams()); + $this->assertTrue($command->needReturnCode()); $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); From 52aa78c5ae0dd8dda6314cdd9333bc402bd1a8cc Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 14:32:49 +1030 Subject: [PATCH 10/19] Updates and bugfixes --- src/HestiaCP/Client.php | 3 +- src/HestiaCP/Command/Command.php | 21 +--------- src/HestiaCP/Command/ICommand.php | 6 ++- src/HestiaCP/Command/Lists/MailDomainDkim.php | 5 --- .../Command/Lists/MailDomainDkimDns.php | 5 --- src/HestiaCP/Command/ResponseCode.php | 3 +- tests/clientSendTest.php | 38 ++++++++++++++++++- tests/commandListTest.php | 6 ++- 8 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index d489c0c..b0fdc69 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -4,7 +4,6 @@ use neto737\HestiaCP\Authorization\Credentials; use neto737\HestiaCP\Authorization\Host; -use neto737\HestiaCP\Command\Command; use neto737\HestiaCP\Command\ICommand; use neto737\HestiaCP\Command\TestAuthorization; use neto737\HestiaCP\Exceptions\ClientException; @@ -116,7 +115,7 @@ public function send(ICommand $command) { */ private function getTimeout(ICommand $command): float { $timeout = (float) $this->guzzleClient->getConfig('timeout'); - $commandTimeout = $command instanceof Command ? $command->getTimeout() : null; + $commandTimeout = $command->getTimeout(); if ($commandTimeout === null || $timeout == 0) { return $timeout; diff --git a/src/HestiaCP/Command/Command.php b/src/HestiaCP/Command/Command.php index 70905eb..a890864 100644 --- a/src/HestiaCP/Command/Command.php +++ b/src/HestiaCP/Command/Command.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Command; -use GuzzleHttp\Exception\ClientException; use neto737\HestiaCP\Exceptions\AuthorizationException; use neto737\HestiaCP\Exceptions\InvalidResponseException; use neto737\HestiaCP\Exceptions\ProcessException; @@ -28,15 +27,6 @@ abstract class Command implements ICommand { /** @var int|null */ private $lastExitCode; - /** - * @deprecated The client no longer lets Guzzle throw on HTTP errors, so this is not called anymore. - * @param ClientException $exception - * @throws ProcessException - */ - public function processException(ClientException $exception): void { - throw new ProcessException($exception->getMessage(), 0, $exception); - } - public function setLastResponse(ResponseInterface $response): void { $this->lastResponse = (string) $response->getBody(); $this->lastStatusCode = $response->getStatusCode(); @@ -99,15 +89,6 @@ public function defaultProcess(): void { } } - /** - * @param int $code - * @throws AuthorizationException - * @throws ProcessException - */ - protected function throwCodeException($code): void { - throw $this->codeException((int) $code); - } - /** * Builds the exception for a non-zero HestiaCP exit code. */ @@ -120,7 +101,7 @@ protected function codeException(int $code): ProcessException { $message .= ' (' . $detail . ')'; } - if ($code === ResponseCode::E_PASSWORD || $code === ResponseCode::E_FORBIDEN) { + if ($code === ResponseCode::E_PASSWORD || $code === ResponseCode::E_FORBIDDEN) { return new AuthorizationException($message, $code); } diff --git a/src/HestiaCP/Command/ICommand.php b/src/HestiaCP/Command/ICommand.php index a8924ab..815c881 100644 --- a/src/HestiaCP/Command/ICommand.php +++ b/src/HestiaCP/Command/ICommand.php @@ -2,7 +2,6 @@ namespace neto737\HestiaCP\Command; -use GuzzleHttp\Exception\ClientException; use Psr\Http\Message\ResponseInterface; interface ICommand { @@ -15,7 +14,10 @@ public function needReturnCode(): bool; public function process(); - public function processException(ClientException $exception); + /** + * Request timeout in seconds for this command (0 = no timeout), or null to use the client timeout. + */ + public function getTimeout(): ?float; public function getRequestParams(): array; } diff --git a/src/HestiaCP/Command/Lists/MailDomainDkim.php b/src/HestiaCP/Command/Lists/MailDomainDkim.php index 55fe32e..26f92ff 100644 --- a/src/HestiaCP/Command/Lists/MailDomainDkim.php +++ b/src/HestiaCP/Command/Lists/MailDomainDkim.php @@ -21,11 +21,6 @@ public function getName(): string { return 'v-list-mail-domain-dkim'; } - public function getResponseText(): string { - // bug, issue 1827 ( https://github.com/serghey-rodin/vesta/issues/1827 ) - return preg_replace('~\,\s*\}~', '}', (string) parent::getResponseText()); - } - public function getRequestParams(): array { return [ self::ARG_1 => $this->user, diff --git a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php index 10db16c..020063e 100644 --- a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php +++ b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php @@ -21,11 +21,6 @@ public function getName(): string { return 'v-list-mail-domain-dkim-dns'; } - public function getResponseText(): string { - // bug, issue 1620 ( https://github.com/serghey-rodin/vesta/issues/1620 ) - return preg_replace('~\\n~', '', (string) parent::getResponseText()); - } - public function getRequestParams(): array { return [ self::ARG_1 => $this->user, diff --git a/src/HestiaCP/Command/ResponseCode.php b/src/HestiaCP/Command/ResponseCode.php index e501fae..b62fa15 100644 --- a/src/HestiaCP/Command/ResponseCode.php +++ b/src/HestiaCP/Command/ResponseCode.php @@ -14,7 +14,6 @@ class ResponseCode { public const E_INUSE = 7; public const E_LIMIT = 8; public const E_PASSWORD = 9; - public const E_FORBIDEN = 10; public const E_FORBIDDEN = 10; public const E_DISABLED = 11; public const E_PARSING = 12; @@ -41,7 +40,7 @@ class ResponseCode { self::E_INUSE => 'Object can\'t be deleted because is used by the other object', self::E_LIMIT => 'Object cannot be created because of hosting package limits', self::E_PASSWORD => 'Wrong password', - self::E_FORBIDEN => 'Object cannot be accessed by the user', + self::E_FORBIDDEN => 'Object cannot be accessed by the user', self::E_DISABLED => 'Subsystem is disabled', self::E_PARSING => 'Configuration is broken', self::E_DISK => 'Not enough disk space to complete the action', diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index 0f774da..47cfcf1 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -262,6 +262,42 @@ public function testListDatabasesReturnsAll(): void { $this->assertSame(['admin_one', 'admin_two'], array_keys($databases)); } + // output of bin/v-list-mail-domain-dkim in HestiaCP 1.10.5, PEM newlines escaped by the script + public function testListDomainDkim(): void { + $body = <<<'JSON' +{ + "domain.com": { + "PEM": "-----BEGIN PRIVATE KEY-----\nMIIEvQ\n-----END PRIVATE KEY-----\n", + "PUB": "-----BEGIN PUBLIC KEY-----\nMIIBIj\n-----END PUBLIC KEY-----\n" + } +} +JSON; + $client = $this->createClient([new Response(200, [], $body)]); + + $dkim = $client->getModuleMail('admin')->listDomainDkim('domain.com'); + $this->assertSame("-----BEGIN PRIVATE KEY-----\nMIIEvQ\n-----END PRIVATE KEY-----\n", $dkim['domain.com']['PEM']); + $this->assertSame("-----BEGIN PUBLIC KEY-----\nMIIBIj\n-----END PUBLIC KEY-----\n", $dkim['domain.com']['PUB']); + } + + // output of bin/v-list-mail-domain-dkim-dns in HestiaCP 1.10.5 + public function testListDomainDkimDns(): void { + $body = <<<'JSON' +{ + + "mail._domainkey": { + "TTL": "3600", + "TXT": "'v=DKIM1; k=rsa; p=MIIBIj'" + } +} +JSON; + $client = $this->createClient([new Response(200, [], $body)]); + + $this->assertSame( + ['mail._domainkey' => ['TTL' => '3600', 'TXT' => "'v=DKIM1; k=rsa; p=MIIBIj'"]], + $client->getModuleMail('admin')->listDomainDkimDns('domain.com') + ); + } + public function testBackup(): void { $client = $this->createClient([self::codeResponse(0), self::codeResponse(0)]); @@ -289,7 +325,7 @@ public function testAuthorizationValidLegacy(): void { public function testAuthorizationInvalid(): void { $client = $this->createClient([ self::codeResponse(ResponseCode::E_PASSWORD, 401), - self::codeResponse(ResponseCode::E_FORBIDEN, 401) + self::codeResponse(ResponseCode::E_FORBIDDEN, 401) ]); $this->assertFalse($client->testAuthorization()); diff --git a/tests/commandListTest.php b/tests/commandListTest.php index 7417ba4..7374d29 100644 --- a/tests/commandListTest.php +++ b/tests/commandListTest.php @@ -56,14 +56,16 @@ public function testMailDomainDkim(): void { $command = new MailDomainDkim('admin', 'domain'); $this->assertSame('v-list-mail-domain-dkim', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'domain', 'arg3' => 'json'], $command->getRequestParams()); - $this->assertSame('', $command->getResponseText()); + + $this->assertSame([], $command->process()); } public function testMailDomainDkimDns(): void { $command = new MailDomainDkimDns('admin', 'domain'); $this->assertSame('v-list-mail-domain-dkim-dns', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'domain', 'arg3' => 'json'], $command->getRequestParams()); - $this->assertSame('', $command->getResponseText()); + + $this->assertSame([], $command->process()); } public function testMailAccounts(): void { From 415d821bf6968a648b44d0a956524296ebafc958 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 14:35:53 +1030 Subject: [PATCH 11/19] Added changelog and updated readme --- CHANGELOG.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 20 ++++++------- 2 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..236ed15 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,82 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +This will be released as 2.0.0. It brings the library up to date with HestiaCP 1.10 and contains breaking changes, marked **BC break** below. + +### Added + +- Database module (`Client::getModuleDatabase()`): `add()`, `delete()`, `deleteDatabases()`, `listDatabase()` and `listDatabases()`. +- Access-key authentication (HestiaCP 1.6+). Named constructors `Credentials::accessKey($key, $secret)`, `Credentials::legacyHash($hash)` and `Credentials::password($user, $pass)`. `ACCESS_KEY:SECRET_KEY` can also be passed as a single string to `Client::simpleFactory()`. +- `verify` client option: `true` (default), `false`, or a path to a CA bundle. +- Missing exit codes in `ResponseCode` (`E_BACKUP`, `E_API_DISABLED`, `E_FORBIDDEN`, and others), with a generic message for unknown codes. +- `Add\LetsEncryptDomain`: `mail` and `keyAlgo` arguments (`ecdsa-256`, `ecdsa-384` or `rsa`). `keyAlgo` is sent only when set, because HestiaCP 1.10.5 doesn't support it yet. +- `Add\User`: optional `name` and `lastname` arguments. +- `Delete\UserBackupExclusions`: optional `system` argument. +- Tests for the client and command processing, using Guzzle's `MockHandler`. +- GitHub Actions CI on PHP 8.1 to 8.5, plus a lowest-dependencies run on PHP 8.1. + +### Changed + +- **BC break:** requires PHP 8.1 or later. +- **BC break:** list and detail methods return plain associative arrays instead of `Nette\Utils\ArrayHash`. Replace `$item->FIELD` with `$item['FIELD']`. +- **BC break:** exceptions moved from `neto737\HestiaCP\*` to `neto737\HestiaCP\Exceptions\*`, one class per file. Autoloading switched from `classmap` to PSR-4. +- **BC break:** TLS certificates are verified by default. For a self-signed certificate, pass `'verify' => false` or a CA bundle path. +- **BC break:** the hostname must include an `http://` or `https://` scheme. A port, path, query, fragment or user info in it throws `InvalidArgumentException`. A trailing slash is removed, and the scheme and host are lowercased. +- **BC break:** the 4th argument of `Webs::addDomainLetsEncrypt()` is now `$mail` instead of the unused `$restart`. +- **BC break:** `getTimeout()` is part of `ICommand`. Classes that implement `ICommand` directly must add it. +- Errors are read from HestiaCP's HTTP status and `Hestia-Exit-Code` header instead of Guzzle exceptions. Exit codes 9 and 10 throw `AuthorizationException`; other non-zero codes throw `ProcessException`; network and timeout failures throw `ClientException` with the original exception as `previous`. +- **BC break:** `Backups::backup()` and `Users::backup()` return `bool` instead of `string`. The backup scripts print a plain-text log, not JSON, so the old return value could not be parsed. +- `Databases::listDatabases()` returns every database instead of only the first one. +- `Add\User` sends a null package as `default`, so the arguments after it keep their positions. +- `Credentials` getters return `?string`. +- Guzzle requirement is `^7.15.2 || ^8.2`. PHPUnit is `^10.5 || ^11.5 || ^12.0`. +- `minimum-stability` is `stable` instead of `dev`. +- Implicit nullable parameters (`string $x = null`) replaced with `?string`. +- Documentation links point to `hestiacp.com/docs`. + +### Removed + +- **BC break:** `nette/utils` dependency. +- **BC break:** `ICommand::processException()` and `Command::processException()`, the unused `Command::throwCodeException()`, and the misspelled `ResponseCode::E_FORBIDEN` alias (use `E_FORBIDDEN`). +- VestaCP-era JSON workarounds in `Lists\MailDomainDkim` and `Lists\MailDomainDkimDns`. HestiaCP outputs valid JSON for both. +- Travis CI configuration. + +### Fixed + +- `TestAuthorization` sent an empty command, which current HestiaCP rejects. It now runs `v-list-sys-info` and returns `false` on an authentication failure. +- `Backup\User` and `Backup\Users` failed because they expected JSON. `Backup\Users` also sent a `json` argument the script doesn't take. +- PHP 8.1 deprecation warnings from passing `null` to `preg_match()` and `preg_replace()`. + +## [1.2.0] - 2021-10-13 + +### Added + +- Backup module (`Client::getModuleBackup()`): create, delete and list backups, and list and delete backup exclusions. +- `Users::backup()` to back up all users. + +## [1.1.0] - 2021-09-10 + +### Added + +- API key authentication: pass the key as the only credential to `Client::simpleFactory()` or `Credentials`. + +## [1.0.1] - 2021-07-01 + +### Fixed + +- `Unsuspend\MailAccount` called `v-suspend-mail-account` instead of `v-unsuspend-mail-account`. + +## [1.0.0] - 2021-06-30 + +First release, with the user, web and mail modules. + +[Unreleased]: https://github.com/neto737/HestiaCP-API/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/neto737/HestiaCP-API/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/neto737/HestiaCP-API/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.com/neto737/HestiaCP-API/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/neto737/HestiaCP-API/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 261feb8..75fc610 100644 --- a/README.md +++ b/README.md @@ -235,26 +235,24 @@ d) db module ```php -$dbModule = $client->getModuleDatabase('admin'); +$dbModule = $client->getModuleDatabase('admin'); // db module needs user +$dbModule->add('database', 'dbuser', 'dbpass'); // creates admin_database with the user admin_dbuser (type defaults to mysql) -$dbModule->add('database', 'dbuser', 'dbpass'); +$dbModule->add('database', 'dbuser', 'dbpass', 'pgsql'); // PostgreSQL, if it is installed on the server -$dbModule->delete('admin_database'); +$dbModule->listDatabases(); // ['admin_database' => ['DATABASE' => '...', 'DBUSER' => '...', ...], ...] -$dbModule->deleteDatabases(); +$dbModule->listDatabase('admin_database'); // returns the database parameters -$dbModule->listDatabase('database'); +$dbModule->delete('admin_database'); // delete one database -$dbModule->listDatabases(); - - -// todo - -// ... etc +$dbModule->deleteDatabases(); // delete all databases of the user ``` +> HestiaCP prefixes the database name and the database user with the owner's username. `add()` takes the names without the prefix; the other methods take the full name (`admin_database`). + e) backup module ```php From 58fa14d1cfd950b88f27628f5c356dae42c05200 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 14:44:33 +1030 Subject: [PATCH 12/19] Updated docs --- CHANGELOG.md | 2 +- README.md | 278 ++++------------------------------------- UPGRADING.md | 126 +++++++++++++++++++ docs/authentication.md | 100 +++++++++++++++ docs/backups.md | 44 +++++++ docs/commands.md | 110 ++++++++++++++++ docs/databases.md | 39 ++++++ docs/errors.md | 68 ++++++++++ docs/mail.md | 52 ++++++++ docs/users.md | 56 +++++++++ docs/web.md | 68 ++++++++++ 11 files changed, 691 insertions(+), 252 deletions(-) create mode 100644 UPGRADING.md create mode 100644 docs/authentication.md create mode 100644 docs/backups.md create mode 100644 docs/commands.md create mode 100644 docs/databases.md create mode 100644 docs/errors.md create mode 100644 docs/mail.md create mode 100644 docs/users.md create mode 100644 docs/web.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 236ed15..1f33bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ This will be released as 2.0.0. It brings the library up to date with HestiaCP 1 - **BC break:** exceptions moved from `neto737\HestiaCP\*` to `neto737\HestiaCP\Exceptions\*`, one class per file. Autoloading switched from `classmap` to PSR-4. - **BC break:** TLS certificates are verified by default. For a self-signed certificate, pass `'verify' => false` or a CA bundle path. - **BC break:** the hostname must include an `http://` or `https://` scheme. A port, path, query, fragment or user info in it throws `InvalidArgumentException`. A trailing slash is removed, and the scheme and host are lowercased. -- **BC break:** the 4th argument of `Webs::addDomainLetsEncrypt()` is now `$mail` instead of the unused `$restart`. +- **BC break:** the 4th argument of `Webs::addDomainLetsEncrypt()` is renamed from `$restart` to `$mail`, which is how HestiaCP has always read it. Positional calls behave the same; named arguments must use the new name. - **BC break:** `getTimeout()` is part of `ICommand`. Classes that implement `ICommand` directly must add it. - Errors are read from HestiaCP's HTTP status and `Hestia-Exit-Code` header instead of Guzzle exceptions. Exit codes 9 and 10 throw `AuthorizationException`; other non-zero codes throw `ProcessException`; network and timeout failures throw `ClientException` with the original exception as `previous`. - **BC break:** `Backups::backup()` and `Users::backup()` return `bool` instead of `string`. The backup scripts print a plain-text log, not JSON, so the old return value could not be parsed. diff --git a/README.md b/README.md index 75fc610..f51ec4a 100644 --- a/README.md +++ b/README.md @@ -1,279 +1,55 @@ - # HestiaCP PHP API - [![Name](https://badgen.net/packagist/name/neto737/hestiacp-api)](//packagist.org/packages/neto737/hestiacp-api) [![Latest Stable Version](https://badgen.net/packagist/v/neto737/hestiacp-api/latest)](https://packagist.org/packages/neto737/hestiacp-api) [![Total Downloads](https://badgen.net/packagist/dt/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![License](https://badgen.net/packagist/license/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![Tests](https://github.com/neto737/HestiaCP-API/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/neto737/HestiaCP-API/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/neto737/HestiaCP-API/branch/main/graph/badge.svg?token=F93I5JQXY6)](https://codecov.io/gh/neto737/HestiaCP-API) - - -## How to use - -1) Installation - -```sh - -$ composer require neto737/hestiacp-api - -``` - -2) Create Client - -a) Easy way - -```php - -use neto737\HestiaCP\Client; - -// Easy way to create Client - -// Using an access key (HestiaCP 1.6+), written as ACCESS_KEY:SECRET_KEY -$client = Client::simpleFactory('https://someHost', 'ACCESS_KEY:SECRET_KEY'); - -// Using a legacy API key -$client = Client::simpleFactory('https://someHost', 'API_Key'); +A PHP client for the [HestiaCP](https://hestiacp.com) API. It manages users, web domains, mail, databases and backups on a HestiaCP server. -// Using username and password (HestiaCP only accepts the admin/root user) -$client = Client::simpleFactory('https://someHost', 'admin', 'somePass'); +## Requirements -``` - -The hostname must start with `https://` (or `http://`) and contain only the host, for example `https://panel.example.com`. An IP address works too: `https://192.0.2.10`, or `https://[2001:db8::1]` for IPv6 (the brackets are required). A trailing slash is removed. The port is a separate argument of `Host` (default `8083`). An invalid hostname throws `\neto737\HestiaCP\Exceptions\InvalidArgumentException`. - -The API has to be enabled in the HestiaCP server settings, and the IP address of the machine that calls it has to be allowed. +- PHP 8.1 or later +- HestiaCP with the API enabled, and the IP address of the calling machine allowed in the server settings -Client options can be passed as the last argument of `simpleFactory()` or `new Client()`: - -```php - -$client = Client::simpleFactory('https://someHost', 'ACCESS_KEY:SECRET_KEY', null, [ - 'timeout' => 30, // seconds, default 10 - 'verify' => false, // default true; set to false for a self-signed certificate, or give a path to a CA bundle -]); +## Installation +```sh +composer require neto737/hestiacp-api ``` -> TLS certificates are verified by default. Older versions of this library never checked them. If your panel uses the self-signed certificate created at install, either install a valid certificate (for example with Let's Encrypt) or pass `'verify' => false`. - -b) For some reasons (more hosts, etc) you may need create objects alone +## Quick start ```php - use neto737\HestiaCP\Client; -use neto737\HestiaCP\Authorization\Credentials; -use neto737\HestiaCP\Authorization\Host; - -// You can choose to use an access key, a legacy API key, or username and password - -// Access key (HestiaCP 1.6+) -$credentials = Credentials::accessKey('ACCESS_KEY', 'SECRET_KEY'); - -// Legacy API Key -$credentials = Credentials::legacyHash('API_Key'); - -// Username and Password (HestiaCP only accepts the admin/root user) -$credentials = Credentials::password('admin', 'somePassword'); - -$host = new Host('https://someHost', $credentials); - -$client = new Client($host); - -``` -3) Usage +// Access key (HestiaCP 1.6+), written as ACCESS_KEY:SECRET_KEY +$client = Client::simpleFactory('https://panel.example.com', 'ACCESS_KEY:SECRET_KEY'); -```php - -// verify login (runs v-list-sys-info, so the credentials must be allowed to run it) +// Check that the credentials work $client->testAuthorization(); // bool -``` - -Errors are thrown as exceptions. The HestiaCP exit code is available as the exception code (see `\neto737\HestiaCP\Command\ResponseCode`): - -```php - -use neto737\HestiaCP\Exceptions\AuthorizationException; -use neto737\HestiaCP\Exceptions\ClientException; -use neto737\HestiaCP\Exceptions\ProcessException; - -try { - $client->getModuleUser()->add('other_user', 'pa$$word', 'some@email.com'); -} catch (AuthorizationException $e) { - // bad credentials, or not allowed to run the command (exit code 9 or 10) -} catch (ProcessException $e) { - // the command failed, e.g. $e->getCode() === ResponseCode::E_EXISTS -} catch (ClientException $e) { - // connection problem (timeout, DNS, TLS, ...) -} - -``` - -You can simply send one of prepared commands (or you can write own command - must implements `\neto737\HestiaCP\Command\ICommand` ) - -```php - -$command = new SomeCommand(); - -$response = $client->send($command); - - -echo $response->getResponseText(); - -``` - -Or you can use prepared modules - -List and detail methods return plain associative arrays decoded from HestiaCP's JSON, keyed by the object name: - -```php - +// List all users $users = $client->getModuleUser()->list(); // ['admin' => ['NAME' => '...', 'CONTACT' => '...', ...], ...] -$admin = $client->getModuleUser()->detail('admin'); -echo $admin['CONTACT']; - -``` - -> Before v2.0 these methods returned `Nette\Utils\ArrayHash` objects. To upgrade, replace `$item->FIELD` with `$item['FIELD']`. - -a) user module - -```php - -$userModule = $client->getModuleUser(); - - -$userModule->list(); // returns all users with data - -$userModule->detail('admin'); // returns selected user with data - -$userModule->changePassword('admin', 'otherPa$$word'); - -$userModule->add('other_user', 'pa$$word', 'some@email.com'); - -$userModule->add('other_user', 'pa$$word', 'some@email.com', 'default', 'John', 'Doe'); // with package, first name and last name - -$userModule->suspend('other_user'); - -$userModule->unsuspend('other_user'); - -$userModule->delete('other_user'); - -``` - -b) web module - -```php - -$webModule = $client->getModuleWeb('admin'); // web module needs user - - -$webModule->listDomains(); - -$webModule->addDomain('domain.com'); - -$webModule->addDomainLetsEncrypt('domain.com', 'www.domain.com'); // needs longer timeout - -$webModule->addDomainLetsEncrypt('domain.com', null, true); // certificate for the mail domain - -$webModule->addDomainLetsEncrypt('domain.com', 'www.domain.com', false, 'rsa'); // key algorithm: ecdsa-256 (default), ecdsa-384 or rsa (needs HestiaCP newer than 1.10.5) - -$webModule->deleteDomainLetsEncrypt('domain.com'); - -$webModule->addDomainFtp('domain.com', 'test', 'pa$$word'); - -$webModule->changeDomainFtpPassword('domain.com', 'admin_test', 'otherPa$$word'); - -$webModule->changeDomainFtpPath('domain.com', 'admin_test', 'path/other'); - -$webModule->deleteDomainFtp('domain.com', 'admin_test'); - -$webModule->suspendDomain('domain.com'); - -$webModule->unsuspendDomain('domain.com'); - -$webModule->deleteDomain('domain.com'); - -``` - -c) mail module - -```php - -$mailModule = $client->getModuleMail('admin'); // mail module needs user - - -$mailModule->listDomains(); // returns mail domains from selected user - -$mailModule->addDomain('domain.com'); // add domain - -$mailModule->listAccounts('domain.com'); // returns accounts from selected user and domain - -$mailModule->listDomainDkim('domain.com'); - -$mailModule->listDomainDkimDns('domain.com'); - -$mailModule->addAccount('domain.com', 'info', 'pa$$word'); // add info@domain.com account - -$mailModule->changeAccountPassword('domain.com', 'info', 'otherPa$$word'); // change info@domain.com password - -$mailModule->suspendAccount('domain.com', 'info'); // suspend info@domain.com account - -$mailModule->unsuspendAccount('domain.com', 'info'); // unsuspend info@domain.com account - -$mailModule->deleteAccount('domain.com', 'info'); - -$mailModule->suspendDomain('domain.com'); - -$mailModule->unsuspendDomain('domain.com'); - -$mailModule->deleteDomain('domain.com'); - -``` - -d) db module - -```php - -$dbModule = $client->getModuleDatabase('admin'); // db module needs user - -$dbModule->add('database', 'dbuser', 'dbpass'); // creates admin_database with the user admin_dbuser (type defaults to mysql) - -$dbModule->add('database', 'dbuser', 'dbpass', 'pgsql'); // PostgreSQL, if it is installed on the server - -$dbModule->listDatabases(); // ['admin_database' => ['DATABASE' => '...', 'DBUSER' => '...', ...], ...] - -$dbModule->listDatabase('admin_database'); // returns the database parameters - -$dbModule->delete('admin_database'); // delete one database - -$dbModule->deleteDatabases(); // delete all databases of the user - +// Add a web domain for the admin user +$client->getModuleWeb('admin')->addDomain('example.com'); ``` -> HestiaCP prefixes the database name and the database user with the owner's username. `add()` takes the names without the prefix; the other methods take the full name (`admin_database`). +TLS certificates are verified by default. If your panel still uses the self-signed certificate created at install, see [TLS certificates](docs/authentication.md#tls-certificates). -e) backup module +Failures are thrown as exceptions (`AuthorizationException`, `ProcessException`, `ClientException`). See [Error handling](docs/errors.md). -```php +## Documentation -$backupModule = $client->getModuleBackup('admin'); // backup module needs user +- [Authentication and client setup](docs/authentication.md): access keys, legacy API keys, passwords, hostname rules, TLS and timeouts +- [Error handling](docs/errors.md): exception types and HestiaCP exit codes +- Modules: + - [Users](docs/users.md) + - [Web domains](docs/web.md) + - [Mail](docs/mail.md) + - [Databases](docs/databases.md) + - [Backups](docs/backups.md) +- [Sending commands directly](docs/commands.md): using the command classes, and writing your own -$backupModule->backup(); // create a new backup (waits until the backup is done, no timeout) - -$backupModule->delete('admin.2021-10-13_18-12-53.tar'); // delete an user backup - -$backupModule->deleteExclusions(); // delete all backup exclusions - -$backupModule->deleteExclusions('WEB'); // delete only one type of exclusion (WEB, DNS, MAIL, DB, CRON or USER) - -$backupModule->listBackups(); // returns the backups list - -$backupModule->listBackup('admin.2021-10-13_18-12-53.tar'); // returns backup parameters list - -$backupModule->listBackupExclusions(); // returns the backup exclusions list - -``` +Upgrading from v1? Read [UPGRADING.md](UPGRADING.md). All changes are listed in [CHANGELOG.md](CHANGELOG.md). ## Donate :heart: diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..a4a94cb --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,126 @@ +# Upgrading + +## From v1 to v2 + +v2 brings the library up to date with HestiaCP 1.10. Most code keeps working, but check each section below. The full list of changes is in [CHANGELOG.md](CHANGELOG.md). + +### PHP 8.1 or later + +v2 requires PHP 8.1. v1 ran on PHP 7.3+. + +The `nette/utils` dependency is gone. If your own code uses Nette Utils, require it in your `composer.json` yourself. + +### List and detail methods return arrays + +List and detail methods (`Users::list()`, `Webs::listDomains()`, `Backups::listBackups()` and so on) now return plain associative arrays instead of `Nette\Utils\ArrayHash` objects. + +```php +// v1 +$admin = $client->getModuleUser()->detail('admin'); +echo $admin->CONTACT; + +// v2 +echo $admin['CONTACT']; +``` + +Search your code for `->` on results of these methods and replace it with array access. `foreach` over the results works as before. + +### Exceptions moved to their own namespace + +The exception classes moved from `neto737\HestiaCP` to `neto737\HestiaCP\Exceptions`. The class names are unchanged. + +```php +// v1 +use neto737\HestiaCP\ProcessException; + +// v2 +use neto737\HestiaCP\Exceptions\ProcessException; +``` + +This applies to `Exception`, `InvalidArgumentException`, `RuntimeException`, `ProcessException`, `ClientException`, `InvalidResponseException` and `AuthorizationException`. A `catch` with the old name doesn't fail with an error: it simply never matches, so the exception goes uncaught. Search for every `use` and `catch` of these classes. + +### Exceptions carry the HestiaCP exit code + +In v1, a failed command threw a `ProcessException` with Guzzle's HTTP error message and code `0`, and a server error could surface as a `ClientException`. In v2: + +- Exit codes 9 and 10 (wrong credentials, access denied) throw `AuthorizationException`. +- Every other failure throws `ProcessException`, with the HestiaCP exit code as `getCode()` and a readable message. +- `ClientException` means only that the request didn't complete (timeout, DNS, TLS, connection refused). + +If you parsed exception messages, compare `$e->getCode()` with the `ResponseCode` constants instead. See [Error handling](docs/errors.md). + +### TLS certificates are verified + +v1 never checked the server's TLS certificate. v2 checks it by default, so a panel with a self-signed certificate now fails with a `ClientException`. + +Either install a valid certificate (for example with Let's Encrypt), or set the `verify` option: + +```php +$client = Client::simpleFactory('https://panel.example.com', 'ACCESS_KEY:SECRET_KEY', null, [ + 'verify' => '/path/to/ca-bundle.pem', // or false to turn verification off +]); +``` + +See [TLS certificates](docs/authentication.md#tls-certificates). + +### The hostname needs a scheme + +The hostname must start with `https://` or `http://` and contain only the host. A hostname without a scheme, or with a port, path, query or fragment, now throws `InvalidArgumentException`. + +v1 passed the hostname through without checking it. + +```php +// v2 rejects these +Client::simpleFactory('panel.example.com', ...); +Client::simpleFactory('https://panel.example.com:8083/', ...); + +// use this instead +Client::simpleFactory('https://panel.example.com', ...); +new Host('https://panel.example.com', $credentials, 8083); // port as a separate argument +``` + +See [Hostname](docs/authentication.md#hostname). + +### `addDomainLetsEncrypt()`: the 4th argument is renamed + +The 4th argument of `Webs::addDomainLetsEncrypt()` was called `$restart`, but HestiaCP has always read it as the MAIL flag: `true` also requested a certificate for the mail domain. v2 renames it to `$mail` to match. Code that passes it by position behaves the same as before. + +```php +// v1 and v2: certificate for the web and mail domain +$web->addDomainLetsEncrypt('example.com', 'www.example.com', true); + +// named arguments must use the new name +$web->addDomainLetsEncrypt('example.com', mail: true); +``` + +If you passed `true` expecting a service restart, pass `false` instead, unless you also want the mail certificate. + +### Backups return `bool` + +`Backups::backup()` and `Users::backup()` return `true` when the backup has finished, instead of a string. In v1 these calls failed anyway, because the library tried to parse the backup log as JSON. + +### `ResponseCode::E_FORBIDEN` removed + +Use the correctly spelled `ResponseCode::E_FORBIDDEN`. + +### Custom commands + +Only relevant if you wrote your own command classes. + +- `getTimeout(): ?float` is now part of `ICommand`. Classes that extend `Command`, `ProcessCommand` or `ListCommand` already have it. Classes that implement `ICommand` directly must add it; return `null` to use the client timeout. +- `processException()` was removed from `ICommand` and `Command`. The client no longer calls it, because HTTP errors no longer throw in Guzzle. Delete any implementation of it. +- `Command::throwCodeException()` was removed. It wasn't used. + +See [Sending commands directly](docs/commands.md). + +### Optional: switch to access keys + +Not required, but recommended. HestiaCP 1.6+ supports access keys, and password login works only for the admin/root user. v1 code that passed `ACCESS_KEY:SECRET_KEY` as a single string keeps working. v2 also has named constructors: + +```php +use neto737\HestiaCP\Authorization\Credentials; + +$credentials = Credentials::accessKey('ACCESS_KEY', 'SECRET_KEY'); +``` + +See [Authentication](docs/authentication.md). diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..4d8dd05 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,100 @@ +# Authentication and client setup + +## Before you start + +The API has to be enabled in the HestiaCP server settings, and the IP address of the machine that calls it has to be allowed. Otherwise every request fails. + +## Credentials + +HestiaCP accepts three kinds of credentials. + +| Kind | HestiaCP version | Notes | +| --- | --- | --- | +| Access key + secret key | 1.6+ | Recommended. Create them in the panel under the user's access keys. | +| Legacy API key | all | The old single `hash` key. | +| Username + password | all | Only accepted for the root/admin user. | + +## Creating a client + +### The short way + +`Client::simpleFactory()` takes the hostname and the credentials as strings: + +```php +use neto737\HestiaCP\Client; + +// Access key, written as ACCESS_KEY:SECRET_KEY +$client = Client::simpleFactory('https://panel.example.com', 'ACCESS_KEY:SECRET_KEY'); + +// Legacy API key +$client = Client::simpleFactory('https://panel.example.com', 'API_KEY'); + +// Username and password (admin/root user only) +$client = Client::simpleFactory('https://panel.example.com', 'admin', 'somePassword'); +``` + +### Building the objects yourself + +Use this when you need a different port, or want to reuse the same credentials for several hosts: + +```php +use neto737\HestiaCP\Client; +use neto737\HestiaCP\Authorization\Credentials; +use neto737\HestiaCP\Authorization\Host; + +$credentials = Credentials::accessKey('ACCESS_KEY', 'SECRET_KEY'); +// or Credentials::legacyHash('API_KEY'); +// or Credentials::password('admin', 'somePassword'); + +$host = new Host('https://panel.example.com', $credentials); // port defaults to 8083 +$host = new Host('https://panel.example.com', $credentials, 443); // a different port + +$client = new Client($host); +``` + +## Hostname + +The hostname must start with `https://` (or `http://`) and contain only the host: + +- `https://panel.example.com` +- `https://192.0.2.10` +- `https://[2001:db8::1]` (IPv6 addresses need the brackets) + +A trailing slash is removed, and the scheme and host are lowercased. The port is a separate argument of `Host`, not part of the hostname. A hostname with a port, path, query, fragment or user info, or without a scheme, throws `\neto737\HestiaCP\Exceptions\InvalidArgumentException`. + +## Client options + +Options are passed as the last argument of `Client::simpleFactory()` or `new Client()`: + +```php +$client = Client::simpleFactory('https://panel.example.com', 'ACCESS_KEY:SECRET_KEY', null, [ + 'timeout' => 30, + 'verify' => '/path/to/ca-bundle.pem', +]); + +$client = new Client($host, ['timeout' => 30]); +``` + +| Option | Default | Description | +| --- | --- | --- | +| `timeout` | `10` | Request timeout in seconds. `0` means no timeout. | +| `verify` | `true` | Verify the TLS certificate. `false` turns verification off; a string is the path to a CA bundle. | +| `handler` | Guzzle default | A custom Guzzle handler stack, for middleware or tests. | + +Some commands need more time and raise the timeout themselves: adding a Let's Encrypt certificate waits at least 90 seconds, and backups have no timeout. A command never uses a shorter timeout than the client's. + +## TLS certificates + +TLS certificates are verified by default. If your panel still uses the self-signed certificate that HestiaCP creates at install, either: + +- install a valid certificate (for example with Let's Encrypt), or +- pass the path to a CA bundle that contains your certificate as `verify`, or +- pass `'verify' => false` to turn verification off. Only do this on a network you trust, because it makes the connection open to interception. + +## Checking the credentials + +```php +$client->testAuthorization(); // true, or false on an authentication failure +``` + +It runs `v-list-sys-info`, so the credentials must be allowed to run that command. diff --git a/docs/backups.md b/docs/backups.md new file mode 100644 index 0000000..11f4870 --- /dev/null +++ b/docs/backups.md @@ -0,0 +1,44 @@ +# Backups + +```php +$backup = $client->getModuleBackup('admin'); +``` + +The backup module works on the backups of one user, given when you get the module. To back up every user at once, use `Users::backup()` in the [user module](users.md). + +## Creating a backup + +```php +$backup->backup(); // true when the backup has finished + +$backup->backup(false); // don't send the user a notification +``` + +The call waits until the backup is done, with no timeout, so it can take a long time on a large account. + +## Listing + +```php +$backup->listBackups(); // ['admin.2021-10-13_18-12-53.tar' => [...], ...] + +$backup->listBackup('admin.2021-10-13_18-12-53.tar'); // the fields of one backup +``` + +## Deleting + +```php +$backup->delete('admin.2021-10-13_18-12-53.tar'); +``` + +## Exclusions + +Exclusions are the parts of the account left out of backups. + +```php +$backup->listBackupExclusions(); + +$backup->deleteExclusions(); // delete all exclusions +$backup->deleteExclusions('WEB'); // delete one type: WEB, DNS, MAIL, DB, CRON or USER +``` + +All methods except the `list*()` methods return `true` on success and throw an exception on failure (see [Error handling](errors.md)). diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..c1b3789 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,110 @@ +# Sending commands directly + +The modules are a convenience layer. Each method creates a command object and passes it to `Client::send()`. You can do the same yourself, with the command classes in `neto737\HestiaCP\Command\*`, or with a command class of your own. + +## Using a command class + +```php +use neto737\HestiaCP\Command\Lists\Users; +use neto737\HestiaCP\Command\Change\UserPassword; + +$users = $client->send(new Users()); // array + +$client->send(new UserPassword('admin', 'otherPa$$word')); // true +``` + +`send()` returns whatever the command's `process()` method returns. The raw response body is kept on the command object: + +```php +$command = new Users(); +$client->send($command); + +echo $command->getResponseText(); // the JSON returned by HestiaCP +echo $command->getStatusCode(); // the HTTP status code +``` + +## Writing your own command + +Extend one of the two base classes: + +- `ProcessCommand` for commands that change something. `process()` returns `true` on exit code 0 and throws an exception otherwise. +- `ListCommand` for commands that return JSON. `process()` returns the decoded array. + +Implement `getName()` (the HestiaCP script name) and `getRequestParams()` (the script's arguments, in order): + +```php +use neto737\HestiaCP\Command\ProcessCommand; + +class AddCronJob extends ProcessCommand { + + public function __construct( + private string $user, + private string $min, + private string $hour, + private string $day, + private string $month, + private string $weekday, + private string $command + ) {} + + public function getName(): string { + return 'v-add-cron-job'; + } + + public function getRequestParams(): array { + return [ + 'arg1' => $this->user, + 'arg2' => $this->min, + 'arg3' => $this->hour, + 'arg4' => $this->day, + 'arg5' => $this->month, + 'arg6' => $this->weekday, + 'arg7' => $this->command, + ]; + } +} + +$client->send(new AddCronJob('admin', '0', '3', '*', '*', '*', '/usr/bin/php /home/admin/cron.php')); +``` + +```php +use neto737\HestiaCP\Command\ListCommand; + +class ListCronJobs extends ListCommand { + + public function __construct(private string $user) {} + + public function getName(): string { + return 'v-list-cron-jobs'; + } + + public function getRequestParams(): array { + return [ + 'arg1' => $this->user, + 'arg2' => 'json', + ]; + } +} + +$jobs = $client->send(new ListCronJobs('admin')); +``` + +Most HestiaCP list scripts print JSON only when you pass `json` as their format argument. + +### Timeouts + +To give a slow command more time than the client default, override `getTimeout()`: + +```php +public function getTimeout(): ?float { + return 120; // seconds; 0 means no timeout, null uses the client timeout +} +``` + +A command can raise the timeout but never lower it below the client's. + +### Implementing `ICommand` directly + +You can implement `\neto737\HestiaCP\Command\ICommand` without extending the base classes. You then have to implement every method yourself, including `setLastResponse()`, `needReturnCode()`, `process()` and `getTimeout()`. Extending `ProcessCommand` or `ListCommand` is simpler, and they already handle HestiaCP's exit codes and error responses. + +The full list of HestiaCP commands and their arguments is in the [HestiaCP CLI reference](https://hestiacp.com/docs/reference/cli). diff --git a/docs/databases.md b/docs/databases.md new file mode 100644 index 0000000..206bb68 --- /dev/null +++ b/docs/databases.md @@ -0,0 +1,39 @@ +# Databases + +```php +$db = $client->getModuleDatabase('admin'); +``` + +The database module works on the databases of one user, given when you get the module. + +## Adding + +```php +$db->add('database', 'dbuser', 'dbpass'); // creates admin_database with the user admin_dbuser + +$db->add('database', 'dbuser', 'dbpass', 'pgsql'); // PostgreSQL, if it is installed on the server +``` + +The type defaults to `mysql`. + +## Listing + +```php +$db->listDatabases(); // ['admin_database' => ['DATABASE' => '...', 'DBUSER' => '...', ...], ...] + +$db->listDatabase('admin_database'); // the fields of one database +``` + +## Deleting + +```php +$db->delete('admin_database'); // one database + +$db->deleteDatabases(); // every database of the user +``` + +## Name prefixes + +HestiaCP prefixes the database name and the database user with the owner's username. `add()` takes the names without the prefix (`database`); the other methods take the full name (`admin_database`). + +All methods except the `list*()` methods return `true` on success and throw an exception on failure (see [Error handling](errors.md)). diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..56a67d0 --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,68 @@ +# Error handling + +Every failure is thrown as an exception. All of them extend `\neto737\HestiaCP\Exceptions\Exception`. + +```php +use neto737\HestiaCP\Command\ResponseCode; +use neto737\HestiaCP\Exceptions\AuthorizationException; +use neto737\HestiaCP\Exceptions\ClientException; +use neto737\HestiaCP\Exceptions\ProcessException; + +try { + $client->getModuleUser()->add('other_user', 'pa$$word', 'some@email.com'); +} catch (AuthorizationException $e) { + // bad credentials, or not allowed to run the command (exit code 9 or 10) +} catch (ProcessException $e) { + if ($e->getCode() === ResponseCode::E_EXISTS) { + // the user already exists + } +} catch (ClientException $e) { + // connection problem (timeout, DNS, TLS, ...) +} +``` + +Catch `AuthorizationException` before `ProcessException`, because it is a subclass. + +## Exception types + +All classes are in the `neto737\HestiaCP\Exceptions` namespace. + +| Exception | Extends | Thrown when | +| --- | --- | --- | +| `AuthorizationException` | `ProcessException` | The credentials are wrong (exit code 9) or the user may not run the command (exit code 10). | +| `InvalidResponseException` | `ProcessException` | The response isn't what HestiaCP sends, for example when the hostname points to another server. | +| `ProcessException` | `RuntimeException` | HestiaCP ran the command and it failed with any other exit code, or a list command returned invalid JSON. | +| `ClientException` | `RuntimeException` | The request didn't complete: timeout, DNS failure, TLS error, refused connection. The original Guzzle exception is available with `getPrevious()`. | +| `InvalidArgumentException` | `Exception` | An argument is invalid before anything is sent, for example a malformed hostname. | + +## Exit codes + +For `ProcessException` and its subclasses, `getCode()` returns the HestiaCP exit code. The codes are constants on `\neto737\HestiaCP\Command\ResponseCode`: + +| Code | Constant | Meaning | +| --- | --- | --- | +| 1 | `E_ARGS` | Not enough arguments provided | +| 2 | `E_INVALID` | Object or argument is not valid | +| 3 | `E_NOTEXIST` | Object doesn't exist | +| 4 | `E_EXISTS` | Object already exists | +| 5 | `E_SUSPENDED` | Object is suspended | +| 6 | `E_UNSUSPENDED` | Object is already unsuspended | +| 7 | `E_INUSE` | Object can't be deleted because another object uses it | +| 8 | `E_LIMIT` | Hosting package limits reached | +| 9 | `E_PASSWORD` | Wrong password | +| 10 | `E_FORBIDDEN` | Object cannot be accessed by the user | +| 11 | `E_DISABLED` | Subsystem is disabled | +| 12 | `E_PARSING` | Configuration is broken | +| 13 | `E_DISK` | Not enough disk space | +| 14 | `E_LA` | Server is too busy | +| 15 | `E_CONNECT` | Connection failed, host is unreachable | +| 16 | `E_FTP` | FTP server is not responding | +| 17 | `E_DB` | Database server is not responding | +| 18 | `E_RRD` | RRDtool failed to update the database | +| 19 | `E_UPDATE` | Update operation failed | +| 20 | `E_RESTART` | Service restart failed | +| 21 | `E_BACKUP` / `E_API_DISABLED` | Backup failed, or API access is disabled for this user | + +HestiaCP uses 21 for two things: `E_BACKUP` in its scripts and `E_API_DISABLED` in the API. Both constants have the value 21. + +An unknown code gets the message `Unknown error (exit code N)`. `ResponseCode::getMessage($code)` returns the message for any code. diff --git a/docs/mail.md b/docs/mail.md new file mode 100644 index 0000000..105fe10 --- /dev/null +++ b/docs/mail.md @@ -0,0 +1,52 @@ +# Mail + +```php +$mail = $client->getModuleMail('admin'); +``` + +The mail module works on the mail domains of one user, given when you get the module. + +## Domains + +```php +$mail->listDomains(); // ['example.com' => [...], ...] + +$mail->addDomain('example.com'); + +$mail->suspendDomain('example.com'); +$mail->unsuspendDomain('example.com'); + +$mail->deleteDomain('example.com'); +``` + +`addDomain()` turns on antispam, antivirus and DKIM by default. Pass `false` to turn any of them off: + +```php +$mail->addDomain('example.com', true, true, false); // antispam, antivirus, no DKIM +``` + +## DKIM + +```php +$mail->listDomainDkim('example.com'); // the domain's DKIM key +$mail->listDomainDkimDns('example.com'); // the DNS record to publish +``` + +## Accounts + +```php +$mail->listAccounts('example.com'); // ['info' => [...], ...] + +$mail->addAccount('example.com', 'info', 'pa$$word'); // creates info@example.com + +$mail->changeAccountPassword('example.com', 'info', 'otherPa$$word'); + +$mail->suspendAccount('example.com', 'info'); +$mail->unsuspendAccount('example.com', 'info'); + +$mail->deleteAccount('example.com', 'info'); +``` + +Accounts are given by the part before the `@`. + +All methods except the `list*()` methods return `true` on success and throw an exception on failure (see [Error handling](errors.md)). diff --git a/docs/users.md b/docs/users.md new file mode 100644 index 0000000..23cf32c --- /dev/null +++ b/docs/users.md @@ -0,0 +1,56 @@ +# Users + +```php +$users = $client->getModuleUser(); +``` + +The user module works on all users, so it takes no username. + +## Listing + +```php +$users->list(); // ['admin' => ['NAME' => '...', 'CONTACT' => '...', ...], ...] + +$admin = $users->detail('admin'); // ['NAME' => '...', 'CONTACT' => '...', ...] +echo $admin['CONTACT']; +``` + +`list()` returns every user, keyed by username. `detail()` returns the fields of one user. + +## Adding + +```php +$users->add('other_user', 'pa$$word', 'some@email.com'); + +// with a package, first name and last name +$users->add('other_user', 'pa$$word', 'some@email.com', 'default', 'John', 'Doe'); +``` + +If the package is `null`, the user gets the `default` package. + +## Changing the password + +```php +$users->changePassword('other_user', 'otherPa$$word'); +``` + +## Suspending and deleting + +```php +$users->suspend('other_user'); +$users->unsuspend('other_user'); + +$users->delete('other_user'); +``` + +`suspend()` and `unsuspend()` take an optional second argument, `$restart` (default `false`), that restarts the affected services. + +## Backing up all users + +```php +$users->backup(); // true when the backup has finished +``` + +This backs up every user on the server and waits until it's done, with no timeout. To back up one user, use the [backup module](backups.md). + +All methods except `list()` and `detail()` return `true` on success and throw an exception on failure (see [Error handling](errors.md)). diff --git a/docs/web.md b/docs/web.md new file mode 100644 index 0000000..dd85c5d --- /dev/null +++ b/docs/web.md @@ -0,0 +1,68 @@ +# Web domains + +```php +$web = $client->getModuleWeb('admin'); +``` + +The web module works on the domains of one user, given when you get the module. + +## Domains + +```php +$web->listDomains(); // ['example.com' => ['IP' => '...', 'ALIAS' => '...', ...], ...] + +$web->addDomain('example.com'); + +$web->suspendDomain('example.com'); +$web->unsuspendDomain('example.com'); + +$web->deleteDomain('example.com'); +``` + +`addDomain()` also takes these optional arguments: + +```php +$web->addDomain( + 'example.com', + '192.0.2.10', // IP address (default: chosen by HestiaCP) + 'www.example.com', // aliases, comma-separated + 'jpg,png,css,js', // file extensions served by the proxy + true // restart the web server +); +``` + +`suspendDomain()` and `unsuspendDomain()` take an optional `$restart` argument (default `false`). + +## Let's Encrypt + +```php +$web->addDomainLetsEncrypt('example.com', 'www.example.com'); + +// certificate for the mail domain as well +$web->addDomainLetsEncrypt('example.com', null, true); + +// key algorithm: ecdsa-256 (default), ecdsa-384 or rsa +$web->addDomainLetsEncrypt('example.com', 'www.example.com', false, 'rsa'); + +$web->deleteDomainLetsEncrypt('example.com'); +``` + +Getting a certificate is slow, so this command uses a timeout of at least 90 seconds. + +The key algorithm argument needs a HestiaCP version newer than 1.10.5. Leave it `null` on older servers, and it won't be sent. + +## FTP accounts + +```php +$web->addDomainFtp('example.com', 'test', 'pa$$word'); +$web->addDomainFtp('example.com', 'test', 'pa$$word', 'path/inside/domain'); // with a path + +$web->changeDomainFtpPassword('example.com', 'admin_test', 'otherPa$$word'); +$web->changeDomainFtpPath('example.com', 'admin_test', 'path/other'); + +$web->deleteDomainFtp('example.com', 'admin_test'); +``` + +HestiaCP prefixes the FTP username with the owner's username. `addDomainFtp()` takes the name without the prefix (`test`); the other methods take the full name (`admin_test`). + +All methods except `listDomains()` return `true` on success and throw an exception on failure (see [Error handling](errors.md)). From 723f2221492652098cfb396b458377265a5db082 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 15:38:49 +1030 Subject: [PATCH 13/19] Updates in readme Added gitattributes --- .gitattributes | 7 +++ .github/logo.svg | 132 +++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 2 +- README.md | 119 ++++++++++++++++++++++++++++++++++++------ composer.json | 28 +++++++++- 5 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/logo.svg diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..df77537 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Keep development files out of the Composer package +/.github export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/ROADMAP.md export-ignore diff --git a/.github/logo.svg b/.github/logo.svg new file mode 100644 index 0000000..a5721e7 --- /dev/null +++ b/.github/logo.svg @@ -0,0 +1,132 @@ + + HestiaCP PHP API + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE index ec887e2..2e606fb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 PROFI CLOUD SERVICES s.r.o. +Copyright (c) 2021-2026 Neto (neto737) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f51ec4a..d10d2e5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,34 @@ -# HestiaCP PHP API - -[![Name](https://badgen.net/packagist/name/neto737/hestiacp-api)](//packagist.org/packages/neto737/hestiacp-api) [![Latest Stable Version](https://badgen.net/packagist/v/neto737/hestiacp-api/latest)](https://packagist.org/packages/neto737/hestiacp-api) [![Total Downloads](https://badgen.net/packagist/dt/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![License](https://badgen.net/packagist/license/neto737/hestiacp-api)](https://packagist.org/packages/neto737/hestiacp-api) [![Tests](https://github.com/neto737/HestiaCP-API/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/neto737/HestiaCP-API/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/neto737/HestiaCP-API/branch/main/graph/badge.svg?token=F93I5JQXY6)](https://codecov.io/gh/neto737/HestiaCP-API) - -A PHP client for the [HestiaCP](https://hestiacp.com) API. It manages users, web domains, mail, databases and backups on a HestiaCP server. +

+ HestiaCP PHP API logo +

+ +

HestiaCP PHP API

+ +

+ A modern PHP client for the HestiaCP API.
+ Manage users, web domains, mail, databases and backups on a HestiaCP server from your PHP code. +

+ +

+ Latest Stable Version + Total Downloads + PHP 8.1 or later + Tests + Coverage + License +

+ +--- + +## Features + +- **Every HestiaCP login method:** access keys (HestiaCP 1.6+), legacy API keys, and admin username and password. +- **Ready-made modules** for users, web domains, mail, databases and backups. +- **Errors as exceptions:** each failure is a typed exception that carries HestiaCP's exit code, so you can tell "already exists" apart from "bad credentials". +- **TLS verification on by default,** with an option for self-signed certificates. +- **Plain PHP arrays** as results. The only dependency is Guzzle. +- **Lower-level access:** send any of the bundled command classes directly, or write your own. +- Tested on PHP 8.1 to 8.5 and checked against HestiaCP 1.10. ## Requirements @@ -29,32 +55,91 @@ $client->testAuthorization(); // bool // List all users $users = $client->getModuleUser()->list(); // ['admin' => ['NAME' => '...', 'CONTACT' => '...', ...], ...] -// Add a web domain for the admin user -$client->getModuleWeb('admin')->addDomain('example.com'); +// Add a web domain for the admin user and give it a Let's Encrypt certificate +$web = $client->getModuleWeb('admin'); +$web->addDomain('example.com'); +$web->addDomainLetsEncrypt('example.com', 'www.example.com'); + +// Create a mailbox +$mail = $client->getModuleMail('admin'); +$mail->addDomain('example.com'); +$mail->addAccount('example.com', 'info', 'S3cure-Pa$$word'); ``` -TLS certificates are verified by default. If your panel still uses the self-signed certificate created at install, see [TLS certificates](docs/authentication.md#tls-certificates). +> [!NOTE] +> TLS certificates are verified by default. If your panel still uses the self-signed certificate created at install, see [TLS certificates](docs/authentication.md#tls-certificates). + +## Modules + +| Module | Get it with | What it covers | +| --- | --- | --- | +| [Users](docs/users.md) | `getModuleUser()` | List, show, add and delete users. Change passwords, suspend and unsuspend, back up all users. | +| [Web domains](docs/web.md) | `getModuleWeb($user)` | Add, delete, suspend and unsuspend domains. Let's Encrypt certificates and FTP accounts. | +| [Mail](docs/mail.md) | `getModuleMail($user)` | Mail domains and accounts, passwords, suspending, DKIM keys and DNS records. | +| [Databases](docs/databases.md) | `getModuleDatabase($user)` | Create, list and delete databases. | +| [Backups](docs/backups.md) | `getModuleBackup($user)` | Create, list and delete backups. List and delete backup exclusions. | + +Need a command that isn't wrapped in a module? See [Sending commands directly](docs/commands.md). -Failures are thrown as exceptions (`AuthorizationException`, `ProcessException`, `ClientException`). See [Error handling](docs/errors.md). +## Error handling + +```php +use neto737\HestiaCP\Command\ResponseCode; +use neto737\HestiaCP\Exceptions\AuthorizationException; +use neto737\HestiaCP\Exceptions\ClientException; +use neto737\HestiaCP\Exceptions\ProcessException; + +try { + $client->getModuleUser()->add('jane', 'S3cure-Pa$$word', 'jane@example.com'); +} catch (AuthorizationException $e) { + // wrong credentials, or not allowed to run the command +} catch (ProcessException $e) { + if ($e->getCode() === ResponseCode::E_EXISTS) { + // the user already exists + } +} catch (ClientException $e) { + // connection problem: timeout, DNS, TLS, ... +} +``` + +All exception types and exit codes are listed in [Error handling](docs/errors.md). ## Documentation - [Authentication and client setup](docs/authentication.md): access keys, legacy API keys, passwords, hostname rules, TLS and timeouts - [Error handling](docs/errors.md): exception types and HestiaCP exit codes -- Modules: - - [Users](docs/users.md) - - [Web domains](docs/web.md) - - [Mail](docs/mail.md) - - [Databases](docs/databases.md) - - [Backups](docs/backups.md) +- Modules: [Users](docs/users.md) · [Web domains](docs/web.md) · [Mail](docs/mail.md) · [Databases](docs/databases.md) · [Backups](docs/backups.md) - [Sending commands directly](docs/commands.md): using the command classes, and writing your own -Upgrading from v1? Read [UPGRADING.md](UPGRADING.md). All changes are listed in [CHANGELOG.md](CHANGELOG.md). +**Upgrading from v1?** Read [UPGRADING.md](UPGRADING.md). All changes are listed in [CHANGELOG.md](CHANGELOG.md). + +## Contributing -## Donate :heart: +Bug reports and pull requests are welcome. To run the test suite: + +```sh +composer install +vendor/bin/phpunit +``` + +The tests use Guzzle's mock handler, so they don't need a HestiaCP server. + +## Credits + +This library started as a fork of [ProfiCloS/vestacp-api](https://github.com/ProfiCloS/vestacp-api) by PROFI CLOUD SERVICES s.r.o., a client for VestaCP, and was adapted and extended for HestiaCP. + +This is a community project. It is not affiliated with or endorsed by the HestiaCP project. The Hestia logo used in this project's logo is copyright [hestiacp.com](https://hestiacp.com) and is included unmodified. + +## Support the project :heart: + +If this library saves you time, you can support its development via [Liberapay](https://liberapay.com/neto737), [PayPal](https://paypal.me/neto737) or crypto: ``` BTC: bc1q89ntljt5lk7g9z68f5cjs83qfm2xme7g4hkur7 ETH: 0xeef9220639F14E7A0FD825AAAd0574e5a8aD7A4B LTC: ltc1q508qfkd09vyya6c5zkfx4r248pf3ezj9ngjdr2 ``` + +## License + +Released under the [MIT License](LICENSE). diff --git a/composer.json b/composer.json index 0c39f49..2fc0ac7 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,38 @@ { "name": "neto737/hestiacp-api", - "description": "HestiaCP API SDK written in PHP", + "description": "PHP client for the HestiaCP API: users, web domains, mail, databases and backups", + "type": "library", + "keywords": [ + "hestiacp", + "hestia", + "hestia-control-panel", + "control-panel", + "web-hosting", + "api", + "api-client", + "sdk" + ], "require": { "php": ">=8.1", "ext-json": "*", "guzzlehttp/guzzle": "^7.15.2 || ^8.2" }, "homepage": "https://github.com/neto737/HestiaCP-API", + "support": { + "issues": "https://github.com/neto737/HestiaCP-API/issues", + "source": "https://github.com/neto737/HestiaCP-API", + "docs": "https://github.com/neto737/HestiaCP-API/tree/main/docs" + }, + "funding": [ + { + "type": "liberapay", + "url": "https://liberapay.com/neto737" + }, + { + "type": "custom", + "url": "https://paypal.me/neto737" + } + ], "license": "MIT", "authors": [ { From 3572b9fe3ce17a5381f7b49651d3f5d3876ffe34 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 15:47:15 +1030 Subject: [PATCH 14/19] Added PHPStan and CS-Fixer --- .gitattributes | 17 ++- .github/workflows/tests.yml | 24 ++++ .gitignore | 2 + .php-cs-fixer.dist.php | 27 ++++ CHANGELOG.md | 1 + README.md | 6 +- composer.json | 23 +++- phpstan.neon.dist | 10 ++ src/HestiaCP/Authorization/Credentials.php | 10 +- src/HestiaCP/Client.php | 12 +- src/HestiaCP/Command/Add/Database.php | 8 +- src/HestiaCP/Command/Backup/User.php | 52 ++++---- src/HestiaCP/Command/Delete/UserBackup.php | 8 +- src/HestiaCP/Command/Lists/Database.php | 8 +- src/HestiaCP/Command/Lists/UserBackup.php | 38 +++--- .../Command/Lists/UserBackupExclusions.php | 28 ++-- src/HestiaCP/Command/Lists/UserBackups.php | 28 ++-- src/HestiaCP/Command/ProcessCommand.php | 1 - src/HestiaCP/Command/Suspend/MailAccount.php | 52 ++++---- src/HestiaCP/Command/Suspend/MailDomain.php | 42 +++--- src/HestiaCP/Command/Suspend/User.php | 42 +++--- src/HestiaCP/Command/Suspend/WebDomain.php | 52 ++++---- .../Command/Unsuspend/MailAccount.php | 52 ++++---- src/HestiaCP/Command/Unsuspend/MailDomain.php | 42 +++--- src/HestiaCP/Command/Unsuspend/User.php | 42 +++--- src/HestiaCP/Command/Unsuspend/WebDomain.php | 52 ++++---- .../Exceptions/AuthorizationException.php | 2 +- src/HestiaCP/Exceptions/ClientException.php | 2 +- src/HestiaCP/Exceptions/Exception.php | 2 +- .../Exceptions/InvalidArgumentException.php | 2 +- .../Exceptions/InvalidResponseException.php | 2 +- src/HestiaCP/Exceptions/ProcessException.php | 2 +- src/HestiaCP/Exceptions/RuntimeException.php | 2 +- src/HestiaCP/Module/Backups.php | 123 +++++++++--------- src/HestiaCP/Module/Databases.php | 109 ++++++++-------- src/HestiaCP/Module/IModule.php | 2 +- src/HestiaCP/Module/Mails.php | 30 ++--- src/HestiaCP/Module/Users.php | 22 ++-- src/HestiaCP/Module/Webs.php | 24 ++-- tests/clientSendTest.php | 1 - tests/commandBackupTest.php | 20 +-- tests/commandListTest.php | 2 +- tests/commandSuspendTest.php | 2 +- tests/commandUnsuspendTest.php | 2 +- 44 files changed, 559 insertions(+), 471 deletions(-) create mode 100644 .php-cs-fixer.dist.php create mode 100644 phpstan.neon.dist diff --git a/.gitattributes b/.gitattributes index df77537..abb4411 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,12 @@ +# PHP files always use LF, which the coding-style check enforces +*.php text eol=lf + # Keep development files out of the Composer package -/.github export-ignore -/tests export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/phpunit.xml.dist export-ignore -/ROADMAP.md export-ignore +/.github export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/ROADMAP.md export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f0d59d3..3c0cd2f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,30 @@ on: workflow_dispatch: jobs: + quality: + name: Coding style and static analysis + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + tools: composer:v2 + + - name: Install dependencies + uses: ramsey/composer-install@v3 + with: + composer-options: --prefer-dist + + - name: Check coding style + run: vendor/bin/php-cs-fixer check --diff + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --no-progress --error-format=github + phpunit: name: PHP ${{ matrix.php }} (${{ matrix.dependencies }} deps) runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index b48fa4b..fc012d1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ test.php /tests/log /.phpunit.cache coverage.xml +/.phpstan.cache +.php-cs-fixer.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..d8b8506 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,27 @@ +in([__DIR__ . '/src', __DIR__ . '/tests']); + +return (new PhpCsFixer\Config()) + ->setRiskyAllowed(false) + ->setIndent("\t") + ->setCacheFile(__DIR__ . '/.php-cs-fixer.cache') + ->setRules([ + '@PSR12' => true, + // Opening braces stay on the same line, with a blank line after a class opening brace + 'braces_position' => [ + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line', + 'anonymous_classes_opening_brace' => 'same_line', + 'anonymous_functions_opening_brace' => 'same_line', + 'control_structures_opening_brace' => 'same_line', + ], + 'no_blank_lines_after_class_opening' => false, + 'array_syntax' => ['syntax' => 'short'], + 'no_unused_imports' => true, + 'single_quote' => true, + 'no_trailing_comma_in_singleline' => true, + 'single_line_empty_body' => true, + ]) + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f33bfd..ab4c504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This will be released as 2.0.0. It brings the library up to date with HestiaCP 1 - `Delete\UserBackupExclusions`: optional `system` argument. - Tests for the client and command processing, using Guzzle's `MockHandler`. - GitHub Actions CI on PHP 8.1 to 8.5, plus a lowest-dependencies run on PHP 8.1. +- PHPStan (level 5) and a PHP-CS-Fixer coding-style check, run in CI. `composer check` runs them with the tests. ### Changed diff --git a/README.md b/README.md index d10d2e5..306e9ab 100644 --- a/README.md +++ b/README.md @@ -115,13 +115,15 @@ All exception types and exit codes are listed in [Error handling](docs/errors.md ## Contributing -Bug reports and pull requests are welcome. To run the test suite: +Bug reports and pull requests are welcome. To run the checks: ```sh composer install -vendor/bin/phpunit +composer check # coding style, static analysis and tests ``` +Each check can also run on its own: `composer test` (PHPUnit), `composer analyse` (PHPStan) and `composer cs` (PHP-CS-Fixer). `composer cs-fix` fixes the coding style. The code uses tabs for indentation and LF line endings. + The tests use Guzzle's mock handler, so they don't need a HestiaCP server. ## Credits diff --git a/composer.json b/composer.json index 2fc0ac7..7f5b029 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,27 @@ } }, "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0" + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0", + "phpstan/phpstan": "^2.1", + "friendsofphp/php-cs-fixer": "^3.75", + "phpstan/phpstan-phpunit": "^2.0" + }, + "scripts": { + "test": "phpunit", + "analyse": "phpstan analyse", + "cs": "php-cs-fixer check --diff", + "cs-fix": "php-cs-fixer fix", + "check": [ + "@cs", + "@analyse", + "@test" + ] + }, + "scripts-descriptions": { + "test": "Run the PHPUnit test suite", + "analyse": "Run PHPStan static analysis", + "cs": "Check the coding style", + "cs-fix": "Fix the coding style", + "check": "Run the coding-style check, static analysis and tests" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..af2c0ce --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,10 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + +parameters: + level: 5 + paths: + - src + - tests + tmpDir: .phpstan.cache diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php index 7bcda55..2113931 100644 --- a/src/HestiaCP/Authorization/Credentials.php +++ b/src/HestiaCP/Authorization/Credentials.php @@ -4,19 +4,19 @@ class Credentials { - /** @var string */ + /** @var string|null */ private $user; - /** @var string */ + /** @var string|null */ private $password; - /** @var string */ + /** @var string|null */ private $api_key; - /** @var string */ + /** @var string|null */ private $access_key; - /** @var string */ + /** @var string|null */ private $secret_key; /** diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index b0fdc69..fe72559 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -36,7 +36,7 @@ class Client { * - handler (callable) Custom Guzzle handler stack (middleware, testing) * * @param Host $host - * @param array|null $options + * @param array|null $options * @throws ClientException */ public function __construct(Host $host, ?array $options = null) { @@ -45,10 +45,10 @@ public function __construct(Host $host, ?array $options = null) { } /** - * @param string $hostname Scheme and host, e.g. https://panel.example.com - * @param string $user Username or API Key - * @param mixed $password Password (must be null if using API Key) - * @param array|null $options See Client::__construct() + * @param string $hostname Scheme and host, e.g. https://panel.example.com + * @param string $user Username or API Key + * @param string|null $password Password (must be null if using API Key) + * @param array|null $options See Client::__construct() * @return Client * @throws ClientException * @throws InvalidArgumentException @@ -125,7 +125,7 @@ private function getTimeout(ICommand $command): float { } /** - * @param array|null $options + * @param array|null $options * @return Client * @throws ClientException */ diff --git a/src/HestiaCP/Command/Add/Database.php b/src/HestiaCP/Command/Add/Database.php index 2945fb1..b28264f 100644 --- a/src/HestiaCP/Command/Add/Database.php +++ b/src/HestiaCP/Command/Add/Database.php @@ -18,15 +18,15 @@ class Database extends ProcessCommand { /** @var string */ private $dbpass; - /** @var string */ - private $type; + /** @var string */ + private $type; public function __construct(string $user, string $database, string $dbuser, string $dbpass, string $type = 'mysql') { $this->user = $user; $this->database = $database; $this->dbuser = $dbuser; $this->dbpass = $dbpass; - $this->type = $type; + $this->type = $type; } public function getName(): string { @@ -39,7 +39,7 @@ public function getRequestParams(): array { self::ARG_2 => $this->database, self::ARG_3 => $this->dbuser, self::ARG_4 => $this->dbpass, - self::ARG_5 => $this->type + self::ARG_5 => $this->type ]; } } diff --git a/src/HestiaCP/Command/Backup/User.php b/src/HestiaCP/Command/Backup/User.php index 5c05b7b..2179870 100644 --- a/src/HestiaCP/Command/Backup/User.php +++ b/src/HestiaCP/Command/Backup/User.php @@ -6,30 +6,30 @@ class User extends ProcessCommand { - /** @var string */ - private $user; - - /** @var bool */ - private $notify; - - public function __construct(string $user, bool $notify = true) { - $this->user = $user; - $this->notify = $notify; - } - - public function getName(): string { - return 'v-backup-user'; - } - - public function getTimeout(): ?float { - // the backup runs synchronously and can take a long time - return 0; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->convertBool($this->notify) - ]; - } + /** @var string */ + private $user; + + /** @var bool */ + private $notify; + + public function __construct(string $user, bool $notify = true) { + $this->user = $user; + $this->notify = $notify; + } + + public function getName(): string { + return 'v-backup-user'; + } + + public function getTimeout(): ?float { + // the backup runs synchronously and can take a long time + return 0; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->convertBool($this->notify) + ]; + } } diff --git a/src/HestiaCP/Command/Delete/UserBackup.php b/src/HestiaCP/Command/Delete/UserBackup.php index b1dac8d..f382cf1 100644 --- a/src/HestiaCP/Command/Delete/UserBackup.php +++ b/src/HestiaCP/Command/Delete/UserBackup.php @@ -9,12 +9,12 @@ class UserBackup extends ProcessCommand { /** @var string */ private $user; - /** @var string */ - private $backup; + /** @var string */ + private $backup; public function __construct(string $user, string $backup) { $this->user = $user; - $this->backup = $backup; + $this->backup = $backup; } public function getName(): string { @@ -24,7 +24,7 @@ public function getName(): string { public function getRequestParams(): array { return [ self::ARG_1 => $this->user, - self::ARG_2 => $this->backup + self::ARG_2 => $this->backup ]; } } diff --git a/src/HestiaCP/Command/Lists/Database.php b/src/HestiaCP/Command/Lists/Database.php index 12d203a..67b1369 100644 --- a/src/HestiaCP/Command/Lists/Database.php +++ b/src/HestiaCP/Command/Lists/Database.php @@ -9,12 +9,12 @@ class Database extends ListCommand { /** @var string */ private $username; - /** @var string */ - private $database; + /** @var string */ + private $database; public function __construct(string $username, string $database) { $this->username = $username; - $this->database = $database; + $this->database = $database; } public function getName(): string { @@ -28,7 +28,7 @@ public function process(): array { public function getRequestParams(): array { return [ self::ARG_1 => $this->username, - self::ARG_2 => $this->database, + self::ARG_2 => $this->database, self::ARG_3 => self::FORMAT_JSON ]; } diff --git a/src/HestiaCP/Command/Lists/UserBackup.php b/src/HestiaCP/Command/Lists/UserBackup.php index 3f0b5c8..36095e3 100644 --- a/src/HestiaCP/Command/Lists/UserBackup.php +++ b/src/HestiaCP/Command/Lists/UserBackup.php @@ -6,30 +6,30 @@ class UserBackup extends ListCommand { - /** @var string */ - private $user; + /** @var string */ + private $user; - /** @var string */ - private $backup; + /** @var string */ + private $backup; - public function __construct(string $user, string $backup) { - $this->user = $user; - $this->backup = $backup; - } + public function __construct(string $user, string $backup) { + $this->user = $user; + $this->backup = $backup; + } - public function getName(): string { - return 'v-list-user-backup'; - } + public function getName(): string { + return 'v-list-user-backup'; + } - public function process(): array { + public function process(): array { return $this->convertDetail(parent::process()); } - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->backup, - self::ARG_3 => self::FORMAT_JSON - ]; - } + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->backup, + self::ARG_3 => self::FORMAT_JSON + ]; + } } diff --git a/src/HestiaCP/Command/Lists/UserBackupExclusions.php b/src/HestiaCP/Command/Lists/UserBackupExclusions.php index ebc3121..b0724a1 100644 --- a/src/HestiaCP/Command/Lists/UserBackupExclusions.php +++ b/src/HestiaCP/Command/Lists/UserBackupExclusions.php @@ -6,21 +6,21 @@ class UserBackupExclusions extends ListCommand { - /** @var string */ - private $user; + /** @var string */ + private $user; - public function __construct(string $user) { - $this->user = $user; - } + public function __construct(string $user) { + $this->user = $user; + } - public function getName(): string { - return 'v-list-user-backup-exclusions'; - } + public function getName(): string { + return 'v-list-user-backup-exclusions'; + } - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => self::FORMAT_JSON - ]; - } + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => self::FORMAT_JSON + ]; + } } diff --git a/src/HestiaCP/Command/Lists/UserBackups.php b/src/HestiaCP/Command/Lists/UserBackups.php index 41627ec..908749d 100644 --- a/src/HestiaCP/Command/Lists/UserBackups.php +++ b/src/HestiaCP/Command/Lists/UserBackups.php @@ -6,21 +6,21 @@ class UserBackups extends ListCommand { - /** @var string */ - private $user; + /** @var string */ + private $user; - public function __construct(string $user) { - $this->user = $user; - } + public function __construct(string $user) { + $this->user = $user; + } - public function getName(): string { - return 'v-list-user-backups'; - } + public function getName(): string { + return 'v-list-user-backups'; + } - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => self::FORMAT_JSON - ]; - } + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => self::FORMAT_JSON + ]; + } } diff --git a/src/HestiaCP/Command/ProcessCommand.php b/src/HestiaCP/Command/ProcessCommand.php index fc3a73b..ba2ae32 100644 --- a/src/HestiaCP/Command/ProcessCommand.php +++ b/src/HestiaCP/Command/ProcessCommand.php @@ -3,7 +3,6 @@ namespace neto737\HestiaCP\Command; use neto737\HestiaCP\Exceptions\AuthorizationException; -use neto737\HestiaCP\Command\Command; use neto737\HestiaCP\Exceptions\ProcessException; abstract class ProcessCommand extends Command { diff --git a/src/HestiaCP/Command/Suspend/MailAccount.php b/src/HestiaCP/Command/Suspend/MailAccount.php index 9f6a839..c1e4152 100644 --- a/src/HestiaCP/Command/Suspend/MailAccount.php +++ b/src/HestiaCP/Command/Suspend/MailAccount.php @@ -6,30 +6,30 @@ class MailAccount extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $domain; - - /** @var string */ - private $account; - - public function __construct(string $user, string $domain, string $account) { - $this->user = $user; - $this->domain = $domain; - $this->account = $account; - } - - public function getName(): string { - return 'v-suspend-mail-account'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->domain, - self::ARG_3 => $this->account - ]; - } + /** @var string */ + private $user; + + /** @var string */ + private $domain; + + /** @var string */ + private $account; + + public function __construct(string $user, string $domain, string $account) { + $this->user = $user; + $this->domain = $domain; + $this->account = $account; + } + + public function getName(): string { + return 'v-suspend-mail-account'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->domain, + self::ARG_3 => $this->account + ]; + } } diff --git a/src/HestiaCP/Command/Suspend/MailDomain.php b/src/HestiaCP/Command/Suspend/MailDomain.php index d518c6b..39f0a89 100644 --- a/src/HestiaCP/Command/Suspend/MailDomain.php +++ b/src/HestiaCP/Command/Suspend/MailDomain.php @@ -6,25 +6,25 @@ class MailDomain extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $domain; - - public function __construct(string $user, string $domain) { - $this->user = $user; - $this->domain = $domain; - } - - public function getName(): string { - return 'v-suspend-mail-domain'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->domain - ]; - } + /** @var string */ + private $user; + + /** @var string */ + private $domain; + + public function __construct(string $user, string $domain) { + $this->user = $user; + $this->domain = $domain; + } + + public function getName(): string { + return 'v-suspend-mail-domain'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->domain + ]; + } } diff --git a/src/HestiaCP/Command/Suspend/User.php b/src/HestiaCP/Command/Suspend/User.php index 44ac897..35615dd 100644 --- a/src/HestiaCP/Command/Suspend/User.php +++ b/src/HestiaCP/Command/Suspend/User.php @@ -6,25 +6,25 @@ class User extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $restart; - - public function __construct(string $user, bool $restart = false) { - $this->user = $user; - $this->restart = $restart; - } - - public function getName(): string { - return 'v-suspend-user'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->convertBool($this->restart) - ]; - } + /** @var string */ + private $user; + + /** @var bool */ + private $restart; + + public function __construct(string $user, bool $restart = false) { + $this->user = $user; + $this->restart = $restart; + } + + public function getName(): string { + return 'v-suspend-user'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->convertBool($this->restart) + ]; + } } diff --git a/src/HestiaCP/Command/Suspend/WebDomain.php b/src/HestiaCP/Command/Suspend/WebDomain.php index 75a8fc4..3d7b64c 100644 --- a/src/HestiaCP/Command/Suspend/WebDomain.php +++ b/src/HestiaCP/Command/Suspend/WebDomain.php @@ -6,30 +6,30 @@ class WebDomain extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $domain; - - /** @var string */ - private $restart; - - public function __construct(string $user, string $domain, bool $restart = false) { - $this->user = $user; - $this->domain = $domain; - $this->restart = $restart; - } - - public function getName(): string { - return 'v-suspend-web-domain'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->domain, - self::ARG_3 => $this->convertBool($this->restart) - ]; - } + /** @var string */ + private $user; + + /** @var string */ + private $domain; + + /** @var bool */ + private $restart; + + public function __construct(string $user, string $domain, bool $restart = false) { + $this->user = $user; + $this->domain = $domain; + $this->restart = $restart; + } + + public function getName(): string { + return 'v-suspend-web-domain'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->domain, + self::ARG_3 => $this->convertBool($this->restart) + ]; + } } diff --git a/src/HestiaCP/Command/Unsuspend/MailAccount.php b/src/HestiaCP/Command/Unsuspend/MailAccount.php index 685d93c..ad8e8f5 100644 --- a/src/HestiaCP/Command/Unsuspend/MailAccount.php +++ b/src/HestiaCP/Command/Unsuspend/MailAccount.php @@ -6,30 +6,30 @@ class MailAccount extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $domain; - - /** @var string */ - private $account; - - public function __construct(string $user, string $domain, string $account) { - $this->user = $user; - $this->domain = $domain; - $this->account = $account; - } - - public function getName(): string { - return 'v-unsuspend-mail-account'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->domain, - self::ARG_3 => $this->account - ]; - } + /** @var string */ + private $user; + + /** @var string */ + private $domain; + + /** @var string */ + private $account; + + public function __construct(string $user, string $domain, string $account) { + $this->user = $user; + $this->domain = $domain; + $this->account = $account; + } + + public function getName(): string { + return 'v-unsuspend-mail-account'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->domain, + self::ARG_3 => $this->account + ]; + } } diff --git a/src/HestiaCP/Command/Unsuspend/MailDomain.php b/src/HestiaCP/Command/Unsuspend/MailDomain.php index 06ef979..3b781f5 100644 --- a/src/HestiaCP/Command/Unsuspend/MailDomain.php +++ b/src/HestiaCP/Command/Unsuspend/MailDomain.php @@ -6,25 +6,25 @@ class MailDomain extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $domain; - - public function __construct(string $user, string $domain) { - $this->user = $user; - $this->domain = $domain; - } - - public function getName(): string { - return 'v-unsuspend-mail-domain'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->domain - ]; - } + /** @var string */ + private $user; + + /** @var string */ + private $domain; + + public function __construct(string $user, string $domain) { + $this->user = $user; + $this->domain = $domain; + } + + public function getName(): string { + return 'v-unsuspend-mail-domain'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->domain + ]; + } } diff --git a/src/HestiaCP/Command/Unsuspend/User.php b/src/HestiaCP/Command/Unsuspend/User.php index d336ac1..254a988 100644 --- a/src/HestiaCP/Command/Unsuspend/User.php +++ b/src/HestiaCP/Command/Unsuspend/User.php @@ -6,25 +6,25 @@ class User extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $restart; - - public function __construct(string $user, bool $restart = false) { - $this->user = $user; - $this->restart = $restart; - } - - public function getName(): string { - return 'v-unsuspend-user'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->convertBool($this->restart) - ]; - } + /** @var string */ + private $user; + + /** @var bool */ + private $restart; + + public function __construct(string $user, bool $restart = false) { + $this->user = $user; + $this->restart = $restart; + } + + public function getName(): string { + return 'v-unsuspend-user'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->convertBool($this->restart) + ]; + } } diff --git a/src/HestiaCP/Command/Unsuspend/WebDomain.php b/src/HestiaCP/Command/Unsuspend/WebDomain.php index 468f8ca..e24578a 100644 --- a/src/HestiaCP/Command/Unsuspend/WebDomain.php +++ b/src/HestiaCP/Command/Unsuspend/WebDomain.php @@ -6,30 +6,30 @@ class WebDomain extends ProcessCommand { - /** @var string */ - private $user; - - /** @var string */ - private $domain; - - /** @var string */ - private $restart; - - public function __construct(string $user, string $domain, bool $restart = false) { - $this->user = $user; - $this->domain = $domain; - $this->restart = $restart; - } - - public function getName(): string { - return 'v-unsuspend-web-domain'; - } - - public function getRequestParams(): array { - return [ - self::ARG_1 => $this->user, - self::ARG_2 => $this->domain, - self::ARG_3 => $this->convertBool($this->restart) - ]; - } + /** @var string */ + private $user; + + /** @var string */ + private $domain; + + /** @var bool */ + private $restart; + + public function __construct(string $user, string $domain, bool $restart = false) { + $this->user = $user; + $this->domain = $domain; + $this->restart = $restart; + } + + public function getName(): string { + return 'v-unsuspend-web-domain'; + } + + public function getRequestParams(): array { + return [ + self::ARG_1 => $this->user, + self::ARG_2 => $this->domain, + self::ARG_3 => $this->convertBool($this->restart) + ]; + } } diff --git a/src/HestiaCP/Exceptions/AuthorizationException.php b/src/HestiaCP/Exceptions/AuthorizationException.php index b0e287c..867f191 100644 --- a/src/HestiaCP/Exceptions/AuthorizationException.php +++ b/src/HestiaCP/Exceptions/AuthorizationException.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class AuthorizationException extends ProcessException { } +class AuthorizationException extends ProcessException {} diff --git a/src/HestiaCP/Exceptions/ClientException.php b/src/HestiaCP/Exceptions/ClientException.php index 96597f5..da0c12b 100644 --- a/src/HestiaCP/Exceptions/ClientException.php +++ b/src/HestiaCP/Exceptions/ClientException.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class ClientException extends RuntimeException { } +class ClientException extends RuntimeException {} diff --git a/src/HestiaCP/Exceptions/Exception.php b/src/HestiaCP/Exceptions/Exception.php index c425f7c..03c88ac 100644 --- a/src/HestiaCP/Exceptions/Exception.php +++ b/src/HestiaCP/Exceptions/Exception.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class Exception extends \Exception { } +class Exception extends \Exception {} diff --git a/src/HestiaCP/Exceptions/InvalidArgumentException.php b/src/HestiaCP/Exceptions/InvalidArgumentException.php index ee2c85e..90cde94 100644 --- a/src/HestiaCP/Exceptions/InvalidArgumentException.php +++ b/src/HestiaCP/Exceptions/InvalidArgumentException.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class InvalidArgumentException extends Exception { } +class InvalidArgumentException extends Exception {} diff --git a/src/HestiaCP/Exceptions/InvalidResponseException.php b/src/HestiaCP/Exceptions/InvalidResponseException.php index 166ba50..54076c9 100644 --- a/src/HestiaCP/Exceptions/InvalidResponseException.php +++ b/src/HestiaCP/Exceptions/InvalidResponseException.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class InvalidResponseException extends ProcessException { } +class InvalidResponseException extends ProcessException {} diff --git a/src/HestiaCP/Exceptions/ProcessException.php b/src/HestiaCP/Exceptions/ProcessException.php index 5c0d6e6..23b21b5 100644 --- a/src/HestiaCP/Exceptions/ProcessException.php +++ b/src/HestiaCP/Exceptions/ProcessException.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class ProcessException extends RuntimeException { } +class ProcessException extends RuntimeException {} diff --git a/src/HestiaCP/Exceptions/RuntimeException.php b/src/HestiaCP/Exceptions/RuntimeException.php index 40af883..8806cac 100644 --- a/src/HestiaCP/Exceptions/RuntimeException.php +++ b/src/HestiaCP/Exceptions/RuntimeException.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Exceptions; -class RuntimeException extends Exception { } +class RuntimeException extends Exception {} diff --git a/src/HestiaCP/Module/Backups.php b/src/HestiaCP/Module/Backups.php index fd95e26..f162b4f 100644 --- a/src/HestiaCP/Module/Backups.php +++ b/src/HestiaCP/Module/Backups.php @@ -3,7 +3,6 @@ namespace neto737\HestiaCP\Module; use neto737\HestiaCP\Client; - use neto737\HestiaCP\Command\Backup\User as BackupUser; use neto737\HestiaCP\Command\Delete\UserBackup as DeleteUserBackup; use neto737\HestiaCP\Command\Delete\UserBackupExclusions as DeleteUserBackupExclusions; @@ -13,81 +12,81 @@ class Backups extends Module { - /** @var string */ + /** @var string */ private $user; - public function __construct(Client $client, string $user) { + public function __construct(Client $client, string $user) { parent::__construct($client); $this->user = $user; } - /** - * This call is used for backing up user with all its domains and databases. - * - * @param bool $notify - * @return bool - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This call is used for backing up user with all its domains and databases. + * + * @param bool $notify + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function backup(bool $notify = true): bool { - return $this->client->send(new BackupUser($this->user, $notify)); - } + */ + public function backup(bool $notify = true): bool { + return $this->client->send(new BackupUser($this->user, $notify)); + } - /** - * This function deletes user backup. - * - * @param string $backup - * @return bool - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function deletes user backup. + * + * @param string $backup + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function delete(string $backup): bool { - return $this->client->send(new DeleteUserBackup($this->user, $backup)); - } + */ + public function delete(string $backup): bool { + return $this->client->send(new DeleteUserBackup($this->user, $backup)); + } - /** - * This function for deleting backup exclusion - * - * @param string|null $system Exclusion type to delete (WEB, DNS, MAIL, DB, CRON or USER). Deletes all of them when not set. - * @return bool - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function for deleting backup exclusion + * + * @param string|null $system Exclusion type to delete (WEB, DNS, MAIL, DB, CRON or USER). Deletes all of them when not set. + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function deleteExclusions(?string $system = null): bool { - return $this->client->send(new DeleteUserBackupExclusions($this->user, $system)); - } + */ + public function deleteExclusions(?string $system = null): bool { + return $this->client->send(new DeleteUserBackupExclusions($this->user, $system)); + } - /** - * This function for obtaining the list of available user backups. - * - * @return array - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function for obtaining the list of available user backups. + * + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function listBackups(): array { - return $this->client->send(new UserBackups($this->user)); - } + */ + public function listBackups(): array { + return $this->client->send(new UserBackups($this->user)); + } - /** - * This function of obtaining the list of backup parameters. - * - * @param string $backup - * @return array - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function of obtaining the list of backup parameters. + * + * @param string $backup + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function listBackup(string $backup): array { - return $this->client->send(new ListsUserBackup($this->user, $backup)); - } + */ + public function listBackup(string $backup): array { + return $this->client->send(new ListsUserBackup($this->user, $backup)); + } - /** - * This function for obtaining the backup exclusion list - * - * @return array - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function for obtaining the backup exclusion list + * + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function listBackupExclusions(): array { - return $this->client->send(new ListsUserBackupExclusions($this->user)); - } -} \ No newline at end of file + */ + public function listBackupExclusions(): array { + return $this->client->send(new ListsUserBackupExclusions($this->user)); + } +} diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php index 877b7b2..cebca83 100644 --- a/src/HestiaCP/Module/Databases.php +++ b/src/HestiaCP/Module/Databases.php @@ -3,7 +3,6 @@ namespace neto737\HestiaCP\Module; use neto737\HestiaCP\Client; - use neto737\HestiaCP\Command\Add\Database as AddDatabase; use neto737\HestiaCP\Command\Delete\Database as DeleteDatabase; use neto737\HestiaCP\Command\Delete\Databases as DeleteDatabases; @@ -12,72 +11,72 @@ class Databases extends Module { - /** @var string */ + /** @var string */ private $user; - public function __construct(Client $client, string $user) { + public function __construct(Client $client, string $user) { parent::__construct($client); $this->user = $user; } - /** - * This function is used for creating a new database. - * - * @param string $database - * @param string $dbuser - * @param string $dbpass - * @param string $type - * @return bool - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function is used for creating a new database. + * + * @param string $database + * @param string $dbuser + * @param string $dbpass + * @param string $type + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function add(string $database, string $dbuser, string $dbpass, string $type = 'mysql'): bool { - return $this->client->send(new AddDatabase($this->user, $database, $dbuser, $dbpass, $type)); - } + */ + public function add(string $database, string $dbuser, string $dbpass, string $type = 'mysql'): bool { + return $this->client->send(new AddDatabase($this->user, $database, $dbuser, $dbpass, $type)); + } - /** - * This function is used for delete a specified database. - * - * @param string $database - * @return bool - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function is used for delete a specified database. + * + * @param string $database + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function delete(string $database): bool { - return $this->client->send(new DeleteDatabase($this->user, $database)); - } + */ + public function delete(string $database): bool { + return $this->client->send(new DeleteDatabase($this->user, $database)); + } - /** - * This function deletes all user databases. - * - * @return bool - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function deletes all user databases. + * + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function deleteDatabases(): bool { - return $this->client->send(new DeleteDatabases($this->user)); - } + */ + public function deleteDatabases(): bool { + return $this->client->send(new DeleteDatabases($this->user)); + } - /** - * This function for obtaining of all database's parameters. - * - * @param string $database - * @return array - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function for obtaining of all database's parameters. + * + * @param string $database + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function listDatabase(string $database): array { - return $this->client->send(new ListDatabase($this->user, $database)); - } + */ + public function listDatabase(string $database): array { + return $this->client->send(new ListDatabase($this->user, $database)); + } - /** - * This function for obtaining the list of all user's databases. - * - * @return array[] - * @throws \neto737\HestiaCP\Exceptions\ClientException + /** + * This function for obtaining the list of all user's databases. + * + * @return array[] + * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException - */ - public function listDatabases(): array { - return $this->client->send(new ListDatabases($this->user)); - } -} \ No newline at end of file + */ + public function listDatabases(): array { + return $this->client->send(new ListDatabases($this->user)); + } +} diff --git a/src/HestiaCP/Module/IModule.php b/src/HestiaCP/Module/IModule.php index 4520c52..806051d 100644 --- a/src/HestiaCP/Module/IModule.php +++ b/src/HestiaCP/Module/IModule.php @@ -2,4 +2,4 @@ namespace neto737\HestiaCP\Module; -interface IModule { } +interface IModule {} diff --git a/src/HestiaCP/Module/Mails.php b/src/HestiaCP/Module/Mails.php index 71702aa..a45e067 100644 --- a/src/HestiaCP/Module/Mails.php +++ b/src/HestiaCP/Module/Mails.php @@ -29,7 +29,7 @@ public function __construct(Client $client, string $user) { /** * This function of obtaining the list of all user domains. - * + * * @param string $domain * @return array[] * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -41,7 +41,7 @@ public function listAccounts(string $domain): array { /** * This function add new email account. - * + * * @param string $domain * @param string $account * @param string $password @@ -55,7 +55,7 @@ public function addAccount(string $domain, string $account, string $password): b /** * This function changes email account password. - * + * * @param string $domain * @param string $account * @param string $password @@ -69,7 +69,7 @@ public function changeAccountPassword(string $domain, string $account, string $p /** * This function suspends mail account. - * + * * @param string $domain * @param string $account * @return bool @@ -77,12 +77,12 @@ public function changeAccountPassword(string $domain, string $account, string $p * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function suspendAccount(string $domain, string $account): bool { - return $this->client->send(new SuspendMailAccount($this->user, $domain, $account)); + return $this->client->send(new SuspendMailAccount($this->user, $domain, $account)); } /** * This function unsuspends mail account. - * + * * @param string $domain * @param string $account * @return bool @@ -90,12 +90,12 @@ public function suspendAccount(string $domain, string $account): bool { * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function unsuspendAccount(string $domain, string $account): bool { - return $this->client->send(new UnsuspendMailAccount($this->user, $domain, $account)); + return $this->client->send(new UnsuspendMailAccount($this->user, $domain, $account)); } /** * This function deletes email account. - * + * * @param string $domain * @param string $account * @return bool @@ -108,7 +108,7 @@ public function deleteAccount(string $domain, string $account): bool { /** * This function adds MAIL domain. - * + * * @param string $domain * @param bool $antispam * @param bool $antivirus @@ -123,7 +123,7 @@ public function addDomain(string $domain, bool $antispam = true, bool $antivirus /** * This function suspends mail domain. - * + * * @param string $domain * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -135,7 +135,7 @@ public function suspendDomain(string $domain): bool { /** * This function unsuspends mail domain. - * + * * @param string $domain * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -147,7 +147,7 @@ public function unsuspendDomain(string $domain): bool { /** * This function for deleting MAIL domain. By deleting it all accounts will also be deleted. - * + * * @param string $domain * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -159,7 +159,7 @@ public function deleteDomain(string $domain): bool { /** * This function of obtaining domain dkim files. - * + * * @param string $domain * @return array[] * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -171,7 +171,7 @@ public function listDomainDkim(string $domain): array { /** * This function of obtaining domain dkim dns records for proper setup. - * + * * @param string $domain * @return array[] * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -183,7 +183,7 @@ public function listDomainDkimDns(string $domain): array { /** * This function of obtaining the list of all user domains. - * + * * @return array[] * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index 135bcfc..1604571 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -5,7 +5,7 @@ use neto737\HestiaCP\Command\Add\User as AddUser; use neto737\HestiaCP\Command\Change\UserPassword; use neto737\HestiaCP\Command\Suspend\User as SuspendUser; -use neto737\HestiaCP\Command\Unsuspend\User as UnsuspendUser; +use neto737\HestiaCP\Command\Unsuspend\User as UnsuspendUser; use neto737\HestiaCP\Command\Delete\User as DeleteUser; use neto737\HestiaCP\Command\Lists\User; use neto737\HestiaCP\Command\Lists\Users as ListsUsers; @@ -15,18 +15,18 @@ class Users extends Module { /** * This function to obtain the list of all system users. - * + * * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function list(): array { - return $this->client->send(new ListsUsers); + return $this->client->send(new ListsUsers()); } /** * This function to obtain user parameters. - * + * * @param string $user * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -38,7 +38,7 @@ public function detail(string $user): array { /** * This function changes user's password and updates RKEY value. - * + * * @param string $user * @param string $password * @return bool @@ -51,7 +51,7 @@ public function changePassword(string $user, string $password): bool { /** * This function creates new user account. - * + * * @param string $user * @param string $password * @param string $email @@ -68,7 +68,7 @@ public function add(string $user, string $password, string $email, ?string $pack /** * This function suspends a certain user and all his objects. - * + * * @param string $user * @param bool $restart * @return bool @@ -81,7 +81,7 @@ public function suspend(string $user, bool $restart = false): bool { /** * This function unsuspends user and all his objects. - * + * * @param string $user * @param bool $restart * @return bool @@ -94,7 +94,7 @@ public function unsuspend(string $user, bool $restart = false): bool { /** * This function deletes a certain user and all his resources such as domains, databases, cron jobs, etc. - * + * * @param string $user * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -106,12 +106,12 @@ public function delete(string $user): bool { /** * This function backups all system users. - * + * * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ public function backup(): bool { - return $this->client->send(new BackupUsers); + return $this->client->send(new BackupUsers()); } } diff --git a/src/HestiaCP/Module/Webs.php b/src/HestiaCP/Module/Webs.php index cbd0422..c0d72ac 100644 --- a/src/HestiaCP/Module/Webs.php +++ b/src/HestiaCP/Module/Webs.php @@ -27,7 +27,7 @@ public function __construct(Client $client, string $user) { /** * This function adds virtual host to a server. - * + * * @param string $domain * @param string|null $ip * @param string|null $aliases @@ -42,8 +42,8 @@ public function addDomain(string $domain, ?string $ip = null, ?string $aliases = } /** - * This function for suspending the site's operation. - * + * This function for suspending the site's operation. + * * @param string $domain * @param bool $restart * @return bool @@ -56,7 +56,7 @@ public function suspendDomain(string $domain, bool $restart = false): bool { /** * This function of unsuspending the domain. - * + * * @param string $domain * @param bool $restart * @return bool @@ -69,7 +69,7 @@ public function unsuspendDomain(string $domain, bool $restart = false): bool { /** * This call of function leads to the removal of domain and all its components (statistics, folders contents, ssl certificates, etc.). - * + * * @param string $domain * @return bool * @throws \neto737\HestiaCP\Exceptions\ClientException @@ -81,7 +81,7 @@ public function deleteDomain(string $domain): bool { /** * This function check and validates domain with Let's Encrypt - * + * * @param string $domain * @param string|null $aliases * @param bool $mail Issue the certificate for the mail domain instead of the web domain @@ -97,7 +97,7 @@ public function addDomainLetsEncrypt(string $domain, ?string $aliases = null, bo /** * This function turns off letsencrypt SSL support for a domain. - * + * * @param string $domain * @param bool $restart * @return bool @@ -110,7 +110,7 @@ public function deleteDomainLetsEncrypt(string $domain, bool $restart = false): /** * This function creates additional ftp account for web domain. - * + * * @param string $domain * @param string $ftpUser * @param string $ftpPassword @@ -125,7 +125,7 @@ public function addDomainFtp(string $domain, string $ftpUser, string $ftpPasswor /** * This function changes ftp user password. - * + * * @param string $domain * @param string $ftpUser * @param string $ftpPassword @@ -139,7 +139,7 @@ public function changeDomainFtpPassword(string $domain, string $ftpUser, string /** * This function changes ftp user path. - * + * * @param string $domain * @param string $ftpUser * @param string $ftpPath @@ -153,7 +153,7 @@ public function changeDomainFtpPath(string $domain, string $ftpUser, string $ftp /** * This function deletes additional ftp account. - * + * * @param string $domain * @param string $ftpUser * @return bool @@ -166,7 +166,7 @@ public function deleteDomainFtp(string $domain, string $ftpUser): bool { /** * This function to obtain the list of all user web domains. - * + * * @return array[] * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index 47cfcf1..46df1c9 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -13,7 +13,6 @@ use neto737\HestiaCP\Exceptions\ClientException; use neto737\HestiaCP\Command\Add\LetsEncryptDomain; use neto737\HestiaCP\Command\Add\User as AddUser; -use neto737\HestiaCP\Command\Lists\Databases; use neto737\HestiaCP\Command\Lists\User; use neto737\HestiaCP\Command\Lists\Users; use neto737\HestiaCP\Command\ResponseCode; diff --git a/tests/commandBackupTest.php b/tests/commandBackupTest.php index 134b500..38db345 100644 --- a/tests/commandBackupTest.php +++ b/tests/commandBackupTest.php @@ -7,21 +7,21 @@ class commandBackupTest extends \PHPUnit\Framework\TestCase { - public function testBackupUsers(): void { - $command = new Users; - $this->assertSame('v-backup-users', $command->getName()); - $this->assertSame([], $command->getRequestParams()); + public function testBackupUsers(): void { + $command = new Users(); + $this->assertSame('v-backup-users', $command->getName()); + $this->assertSame([], $command->getRequestParams()); $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); - } + } - public function testBackupUser(): void { - $command = new User('admin'); - $this->assertSame('v-backup-user', $command->getName()); + public function testBackupUser(): void { + $command = new User('admin'); + $this->assertSame('v-backup-user', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'yes'], $command->getRequestParams()); - $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); - } + } } diff --git a/tests/commandListTest.php b/tests/commandListTest.php index 7374d29..d6a6f90 100644 --- a/tests/commandListTest.php +++ b/tests/commandListTest.php @@ -88,7 +88,7 @@ public function testUserBackup(): void { $command = new UserBackup('admin', 'admin.2021-10-07_05-15-09.tar'); $this->assertSame('v-list-user-backup', $command->getName()); $this->assertSame(['arg1' => 'admin', 'arg2' => 'admin.2021-10-07_05-15-09.tar', 'arg3' => 'json'], $command->getRequestParams()); - + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } diff --git a/tests/commandSuspendTest.php b/tests/commandSuspendTest.php index 19540d9..7427f9e 100644 --- a/tests/commandSuspendTest.php +++ b/tests/commandSuspendTest.php @@ -26,7 +26,7 @@ public function testWebDomain(): void { $this->assertSame([ 'arg1' => 'admin', 'arg2' => 'domain.com', - 'arg3' => 'no' + 'arg3' => 'no' ], $command->getRequestParams()); $this->assertTrue($command->needReturnCode()); diff --git a/tests/commandUnsuspendTest.php b/tests/commandUnsuspendTest.php index 5507e9c..8857c2c 100644 --- a/tests/commandUnsuspendTest.php +++ b/tests/commandUnsuspendTest.php @@ -26,7 +26,7 @@ public function testWebDomain(): void { $this->assertSame([ 'arg1' => 'admin', 'arg2' => 'domain.com', - 'arg3' => 'no' + 'arg3' => 'no' ], $command->getRequestParams()); $this->assertTrue($command->needReturnCode()); From 84ae6f0667924e97c94c0513082b0757296d1ea4 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 15:50:39 +1030 Subject: [PATCH 15/19] Fix bug in tests --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7f5b029..2115cb6 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "require-dev": { "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0", "phpstan/phpstan": "^2.1", - "friendsofphp/php-cs-fixer": "^3.75", + "friendsofphp/php-cs-fixer": "^3.86", "phpstan/phpstan-phpunit": "^2.0" }, "scripts": { From d0027621282f08fa4b9cf74d5389f585f1d65f89 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 15:55:05 +1030 Subject: [PATCH 16/19] Update composer.json --- composer.json | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 2115cb6..a66e210 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,9 @@ "sdk" ], "require": { - "php": ">=8.1", - "ext-json": "*", - "guzzlehttp/guzzle": "^7.15.2 || ^8.2" + "php": "^8.1", + "guzzlehttp/guzzle": "^7.15.2 || ^8.2", + "psr/http-message": "^1.1 || ^2.0" }, "homepage": "https://github.com/neto737/HestiaCP-API", "support": { @@ -47,10 +47,13 @@ } }, "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0", - "phpstan/phpstan": "^2.1", "friendsofphp/php-cs-fixer": "^3.86", - "phpstan/phpstan-phpunit": "^2.0" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0" + }, + "config": { + "sort-packages": true }, "scripts": { "test": "phpunit", From e5c78b77620245ff22dd7ca625bfec5567ac3338 Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 16:05:23 +1030 Subject: [PATCH 17/19] - Added strict_types - Min PHP version required is 8.2 now --- .github/workflows/tests.yml | 4 +-- .php-cs-fixer.dist.php | 3 ++- CHANGELOG.md | 8 +++--- README.md | 6 ++--- UPGRADING.md | 6 +++-- composer.json | 4 +-- src/HestiaCP/Authorization/Credentials.php | 25 ++++++++----------- src/HestiaCP/Authorization/Host.php | 17 ++++++------- src/HestiaCP/Client.php | 24 +++++++++++------- src/HestiaCP/Command/Add/Database.php | 17 ++++++------- .../Command/Add/LetsEncryptDomain.php | 17 ++++++------- src/HestiaCP/Command/Add/MailAccount.php | 14 +++++------ src/HestiaCP/Command/Add/MailDomain.php | 17 ++++++------- src/HestiaCP/Command/Add/User.php | 20 ++++++--------- src/HestiaCP/Command/Add/WebDomain.php | 20 ++++++--------- src/HestiaCP/Command/Add/WebDomainFtp.php | 17 ++++++------- src/HestiaCP/Command/Backup/User.php | 8 +++--- src/HestiaCP/Command/Backup/Users.php | 2 ++ .../Command/Change/MailAccountPassword.php | 14 +++++------ src/HestiaCP/Command/Change/UserPassword.php | 8 +++--- .../Command/Change/WebDomainFtpPassword.php | 14 +++++------ .../Command/Change/WebDomainFtpPath.php | 14 +++++------ src/HestiaCP/Command/Command.php | 11 ++++---- src/HestiaCP/Command/Delete/Database.php | 8 +++--- src/HestiaCP/Command/Delete/Databases.php | 5 ++-- .../Command/Delete/LetsEncryptDomain.php | 11 ++++---- src/HestiaCP/Command/Delete/MailAccount.php | 11 ++++---- src/HestiaCP/Command/Delete/MailDomain.php | 8 +++--- src/HestiaCP/Command/Delete/User.php | 5 ++-- src/HestiaCP/Command/Delete/UserBackup.php | 8 +++--- .../Command/Delete/UserBackupExclusions.php | 8 +++--- src/HestiaCP/Command/Delete/WebDomain.php | 8 +++--- src/HestiaCP/Command/Delete/WebDomainFtp.php | 11 ++++---- src/HestiaCP/Command/ICommand.php | 4 ++- src/HestiaCP/Command/ListCommand.php | 4 ++- src/HestiaCP/Command/Lists/Database.php | 8 +++--- src/HestiaCP/Command/Lists/Databases.php | 5 ++-- src/HestiaCP/Command/Lists/MailAccounts.php | 8 +++--- src/HestiaCP/Command/Lists/MailDomainDkim.php | 8 +++--- .../Command/Lists/MailDomainDkimDns.php | 8 +++--- src/HestiaCP/Command/Lists/MailDomains.php | 5 ++-- src/HestiaCP/Command/Lists/User.php | 5 ++-- src/HestiaCP/Command/Lists/UserBackup.php | 8 +++--- .../Command/Lists/UserBackupExclusions.php | 5 ++-- src/HestiaCP/Command/Lists/UserBackups.php | 5 ++-- src/HestiaCP/Command/Lists/Users.php | 2 ++ src/HestiaCP/Command/Lists/WebDomains.php | 5 ++-- src/HestiaCP/Command/ProcessCommand.php | 2 ++ src/HestiaCP/Command/ResponseCode.php | 4 ++- src/HestiaCP/Command/Suspend/MailAccount.php | 11 ++++---- src/HestiaCP/Command/Suspend/MailDomain.php | 8 +++--- src/HestiaCP/Command/Suspend/User.php | 8 +++--- src/HestiaCP/Command/Suspend/WebDomain.php | 11 ++++---- src/HestiaCP/Command/TestAuthorization.php | 2 ++ .../Command/Unsuspend/MailAccount.php | 11 ++++---- src/HestiaCP/Command/Unsuspend/MailDomain.php | 8 +++--- src/HestiaCP/Command/Unsuspend/User.php | 8 +++--- src/HestiaCP/Command/Unsuspend/WebDomain.php | 11 ++++---- .../Exceptions/AuthorizationException.php | 2 ++ src/HestiaCP/Exceptions/ClientException.php | 2 ++ src/HestiaCP/Exceptions/Exception.php | 2 ++ .../Exceptions/InvalidArgumentException.php | 2 ++ .../Exceptions/InvalidResponseException.php | 2 ++ src/HestiaCP/Exceptions/ProcessException.php | 2 ++ src/HestiaCP/Exceptions/RuntimeException.php | 2 ++ src/HestiaCP/Module/Backups.php | 5 ++-- src/HestiaCP/Module/Databases.php | 5 ++-- src/HestiaCP/Module/IModule.php | 2 ++ src/HestiaCP/Module/Mails.php | 5 ++-- src/HestiaCP/Module/Module.php | 5 ++-- src/HestiaCP/Module/Users.php | 2 ++ src/HestiaCP/Module/Webs.php | 5 ++-- tests/clientSendTest.php | 4 ++- tests/clientTest.php | 2 ++ tests/commandAddTest.php | 2 ++ tests/commandBackupTest.php | 2 ++ tests/commandChangeTest.php | 2 ++ tests/commandDeleteTest.php | 2 ++ tests/commandListTest.php | 2 ++ tests/commandSuspendTest.php | 2 ++ tests/commandTest.php | 2 ++ tests/commandUnsuspendTest.php | 2 ++ tests/credentialTest.php | 16 ++++++------ tests/hostTest.php | 18 +++++++------ tests/moduleTest.php | 2 ++ 85 files changed, 340 insertions(+), 298 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c0cd2f..6221c79 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,10 +37,10 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + php: ['8.2', '8.3', '8.4', '8.5'] dependencies: [highest] include: - - php: '8.1' + - php: '8.2' dependencies: lowest steps: diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index d8b8506..06e6c15 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -4,7 +4,7 @@ ->in([__DIR__ . '/src', __DIR__ . '/tests']); return (new PhpCsFixer\Config()) - ->setRiskyAllowed(false) + ->setRiskyAllowed(true) ->setIndent("\t") ->setCacheFile(__DIR__ . '/.php-cs-fixer.cache') ->setRules([ @@ -23,5 +23,6 @@ 'single_quote' => true, 'no_trailing_comma_in_singleline' => true, 'single_line_empty_body' => true, + 'declare_strict_types' => true, ]) ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4c504..0a39cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,12 @@ This will be released as 2.0.0. It brings the library up to date with HestiaCP 1 - `Add\User`: optional `name` and `lastname` arguments. - `Delete\UserBackupExclusions`: optional `system` argument. - Tests for the client and command processing, using Guzzle's `MockHandler`. -- GitHub Actions CI on PHP 8.1 to 8.5, plus a lowest-dependencies run on PHP 8.1. +- GitHub Actions CI on PHP 8.2 to 8.5, plus a lowest-dependencies run on PHP 8.2. - PHPStan (level 5) and a PHP-CS-Fixer coding-style check, run in CI. `composer check` runs them with the tests. ### Changed -- **BC break:** requires PHP 8.1 or later. +- **BC break:** requires PHP 8.2 or later. PHP 8.1 reached end of life on 31 December 2025. - **BC break:** list and detail methods return plain associative arrays instead of `Nette\Utils\ArrayHash`. Replace `$item->FIELD` with `$item['FIELD']`. - **BC break:** exceptions moved from `neto737\HestiaCP\*` to `neto737\HestiaCP\Exceptions\*`, one class per file. Autoloading switched from `classmap` to PSR-4. - **BC break:** TLS certificates are verified by default. For a self-signed certificate, pass `'verify' => false` or a CA bundle path. @@ -35,9 +35,11 @@ This will be released as 2.0.0. It brings the library up to date with HestiaCP 1 - `Databases::listDatabases()` returns every database instead of only the first one. - `Add\User` sends a null package as `default`, so the arguments after it keep their positions. - `Credentials` getters return `?string`. -- Guzzle requirement is `^7.15.2 || ^8.2`. PHPUnit is `^10.5 || ^11.5 || ^12.0`. +- Guzzle requirement is `^7.15.2 || ^8.2`. PHPUnit is `^11.5 || ^12.0`. - `minimum-stability` is `stable` instead of `dev`. - Implicit nullable parameters (`string $x = null`) replaced with `?string`. +- Every file declares `strict_types=1`. Properties have native types instead of `@var` docblocks. +- **BC break:** `ICommand::process()` returns `mixed`. `ListCommand::process()` returns `array`. The setters in `Client`, `Credentials` and `Host` return `static`. `Module::$client` is typed `Client` and `ResponseCode::$messages` is typed `array`. Custom classes that implement or override these must declare compatible types. - Documentation links point to `hestiacp.com/docs`. ### Removed diff --git a/README.md b/README.md index 306e9ab..1c4ba75 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

Latest Stable Version Total Downloads - PHP 8.1 or later + PHP 8.2 or later Tests Coverage License @@ -28,11 +28,11 @@ - **TLS verification on by default,** with an option for self-signed certificates. - **Plain PHP arrays** as results. The only dependency is Guzzle. - **Lower-level access:** send any of the bundled command classes directly, or write your own. -- Tested on PHP 8.1 to 8.5 and checked against HestiaCP 1.10. +- Tested on PHP 8.2 to 8.5 and checked against HestiaCP 1.10. ## Requirements -- PHP 8.1 or later +- PHP 8.2 or later - HestiaCP with the API enabled, and the IP address of the calling machine allowed in the server settings ## Installation diff --git a/UPGRADING.md b/UPGRADING.md index a4a94cb..dafe93e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -4,9 +4,9 @@ v2 brings the library up to date with HestiaCP 1.10. Most code keeps working, but check each section below. The full list of changes is in [CHANGELOG.md](CHANGELOG.md). -### PHP 8.1 or later +### PHP 8.2 or later -v2 requires PHP 8.1. v1 ran on PHP 7.3+. +v2 requires PHP 8.2. v1 ran on PHP 7.3+. The `nette/utils` dependency is gone. If your own code uses Nette Utils, require it in your `composer.json` yourself. @@ -110,6 +110,8 @@ Only relevant if you wrote your own command classes. - `getTimeout(): ?float` is now part of `ICommand`. Classes that extend `Command`, `ProcessCommand` or `ListCommand` already have it. Classes that implement `ICommand` directly must add it; return `null` to use the client timeout. - `processException()` was removed from `ICommand` and `Command`. The client no longer calls it, because HTTP errors no longer throw in Guzzle. Delete any implementation of it. - `Command::throwCodeException()` was removed. It wasn't used. +- `ICommand::process()` now returns `mixed`. Classes that implement `ICommand` directly must declare a return type, either `mixed` or a narrower one such as `bool` or `array`. `ListCommand::process()` now returns `array`, so subclasses that override it must return `array` too. +- The library is now fully typed. If you extend a library class and override a typed method or redeclare a typed property, use a compatible type: setters in `Client`, `Credentials` and `Host` return `static`, and `Module::$client` is `Client`. See [Sending commands directly](docs/commands.md). diff --git a/composer.json b/composer.json index a66e210..0853854 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "sdk" ], "require": { - "php": "^8.1", + "php": "^8.2", "guzzlehttp/guzzle": "^7.15.2 || ^8.2", "psr/http-message": "^1.1 || ^2.0" }, @@ -50,7 +50,7 @@ "friendsofphp/php-cs-fixer": "^3.86", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.0" + "phpunit/phpunit": "^11.5 || ^12.0" }, "config": { "sort-packages": true diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php index 2113931..31a9f5b 100644 --- a/src/HestiaCP/Authorization/Credentials.php +++ b/src/HestiaCP/Authorization/Credentials.php @@ -1,23 +1,20 @@ access_key = $accessKey; $this->secret_key = $secretKey; $this->api_key = null; @@ -68,7 +65,7 @@ public function getSecretKey(): ?string { return $this->secret_key; } - public function setApiKey(string $key) { + public function setApiKey(string $key): static { $this->api_key = $key; $this->access_key = null; $this->secret_key = null; @@ -79,7 +76,7 @@ public function getApiKey(): ?string { return $this->api_key; } - public function setUser(string $user) { + public function setUser(string $user): static { $this->user = $user; return $this; } @@ -88,7 +85,7 @@ public function getUser(): ?string { return $this->user; } - public function setPassword(string $password) { + public function setPassword(string $password): static { $this->password = $password; return $this; } diff --git a/src/HestiaCP/Authorization/Host.php b/src/HestiaCP/Authorization/Host.php index 4e4d5b3..d55c529 100644 --- a/src/HestiaCP/Authorization/Host.php +++ b/src/HestiaCP/Authorization/Host.php @@ -1,19 +1,18 @@ hostname = self::normalizeHostname($hostname); return $this; } @@ -42,7 +41,7 @@ public function getHostname(): string { return $this->hostname; } - public function setCredentials(Credentials $credentials) { + public function setCredentials(Credentials $credentials): static { $this->credentials = $credentials; return $this; } @@ -51,7 +50,7 @@ public function getCredentials(): Credentials { return $this->credentials; } - public function setPort(int $port) { + public function setPort(int $port): static { $this->port = $port; return $this; } diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index fe72559..a97390f 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -1,5 +1,7 @@ , Module> */ + private array $modules = []; /** * Client constructor. @@ -60,7 +61,7 @@ public static function simpleFactory(string $hostname, string $user, ?string $pa return new self($host, $options); } - public function setHost(Host $host) { + public function setHost(Host $host): static { $this->host = $host; return $this; } @@ -85,7 +86,7 @@ public function testAuthorization(): bool { * @throws ClientException * @throws ProcessException */ - public function send(ICommand $command) { + public function send(ICommand $command): mixed { $host = $this->getHost(); try { $response = $this->guzzleClient->post($host->buildUri(), [ @@ -145,7 +146,12 @@ private function prepareCommunication(?array $options = null): Client { return $this; } - private function loadModule($moduleName, $param = null) { + /** + * @template T of Module + * @param class-string $moduleName + * @return T + */ + private function loadModule(string $moduleName, ?string $param = null): Module { if (!isset($this->modules[$moduleName])) { if ($param !== null) { $this->modules[$moduleName] = new $moduleName($this, $param); diff --git a/src/HestiaCP/Command/Add/Database.php b/src/HestiaCP/Command/Add/Database.php index b28264f..cf2463f 100644 --- a/src/HestiaCP/Command/Add/Database.php +++ b/src/HestiaCP/Command/Add/Database.php @@ -1,25 +1,22 @@ user = $user; diff --git a/src/HestiaCP/Command/Add/LetsEncryptDomain.php b/src/HestiaCP/Command/Add/LetsEncryptDomain.php index dd1e73f..0af7121 100644 --- a/src/HestiaCP/Command/Add/LetsEncryptDomain.php +++ b/src/HestiaCP/Command/Add/LetsEncryptDomain.php @@ -1,5 +1,7 @@ user = $user; diff --git a/src/HestiaCP/Command/Add/MailDomain.php b/src/HestiaCP/Command/Add/MailDomain.php index eaccb15..222ef11 100644 --- a/src/HestiaCP/Command/Add/MailDomain.php +++ b/src/HestiaCP/Command/Add/MailDomain.php @@ -1,25 +1,22 @@ user = $user; diff --git a/src/HestiaCP/Command/Add/User.php b/src/HestiaCP/Command/Add/User.php index b19ec25..bd5acb9 100644 --- a/src/HestiaCP/Command/Add/User.php +++ b/src/HestiaCP/Command/Add/User.php @@ -1,28 +1,24 @@ user = $user; diff --git a/src/HestiaCP/Command/Add/WebDomain.php b/src/HestiaCP/Command/Add/WebDomain.php index 56aa66d..b6ce460 100644 --- a/src/HestiaCP/Command/Add/WebDomain.php +++ b/src/HestiaCP/Command/Add/WebDomain.php @@ -1,28 +1,24 @@ user = $user; diff --git a/src/HestiaCP/Command/Add/WebDomainFtp.php b/src/HestiaCP/Command/Add/WebDomainFtp.php index 26e0dd2..800396a 100644 --- a/src/HestiaCP/Command/Add/WebDomainFtp.php +++ b/src/HestiaCP/Command/Add/WebDomainFtp.php @@ -1,25 +1,22 @@ user = $user; diff --git a/src/HestiaCP/Command/Backup/User.php b/src/HestiaCP/Command/Backup/User.php index 2179870..00bcdc4 100644 --- a/src/HestiaCP/Command/Backup/User.php +++ b/src/HestiaCP/Command/Backup/User.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Backup/Users.php b/src/HestiaCP/Command/Backup/Users.php index e4a5e38..70f5736 100644 --- a/src/HestiaCP/Command/Backup/Users.php +++ b/src/HestiaCP/Command/Backup/Users.php @@ -1,5 +1,7 @@ user = $user; diff --git a/src/HestiaCP/Command/Change/UserPassword.php b/src/HestiaCP/Command/Change/UserPassword.php index 67a832b..87ef5e4 100644 --- a/src/HestiaCP/Command/Change/UserPassword.php +++ b/src/HestiaCP/Command/Change/UserPassword.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Change/WebDomainFtpPassword.php b/src/HestiaCP/Command/Change/WebDomainFtpPassword.php index 33ee6fb..de31b28 100644 --- a/src/HestiaCP/Command/Change/WebDomainFtpPassword.php +++ b/src/HestiaCP/Command/Change/WebDomainFtpPassword.php @@ -1,22 +1,20 @@ user = $user; diff --git a/src/HestiaCP/Command/Change/WebDomainFtpPath.php b/src/HestiaCP/Command/Change/WebDomainFtpPath.php index a9d2e32..b20b917 100644 --- a/src/HestiaCP/Command/Change/WebDomainFtpPath.php +++ b/src/HestiaCP/Command/Change/WebDomainFtpPath.php @@ -1,22 +1,20 @@ user = $user; diff --git a/src/HestiaCP/Command/Command.php b/src/HestiaCP/Command/Command.php index a890864..1b2e2bd 100644 --- a/src/HestiaCP/Command/Command.php +++ b/src/HestiaCP/Command/Command.php @@ -1,5 +1,7 @@ lastResponse = (string) $response->getBody(); diff --git a/src/HestiaCP/Command/Delete/Database.php b/src/HestiaCP/Command/Delete/Database.php index c3cd63b..eb893a3 100644 --- a/src/HestiaCP/Command/Delete/Database.php +++ b/src/HestiaCP/Command/Delete/Database.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/Databases.php b/src/HestiaCP/Command/Delete/Databases.php index c4ba465..45ab345 100644 --- a/src/HestiaCP/Command/Delete/Databases.php +++ b/src/HestiaCP/Command/Delete/Databases.php @@ -1,13 +1,14 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/LetsEncryptDomain.php b/src/HestiaCP/Command/Delete/LetsEncryptDomain.php index a3b8139..99f16d4 100644 --- a/src/HestiaCP/Command/Delete/LetsEncryptDomain.php +++ b/src/HestiaCP/Command/Delete/LetsEncryptDomain.php @@ -1,19 +1,18 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/MailAccount.php b/src/HestiaCP/Command/Delete/MailAccount.php index af653ea..70b97d6 100644 --- a/src/HestiaCP/Command/Delete/MailAccount.php +++ b/src/HestiaCP/Command/Delete/MailAccount.php @@ -1,19 +1,18 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/MailDomain.php b/src/HestiaCP/Command/Delete/MailDomain.php index c0990b1..e57843e 100644 --- a/src/HestiaCP/Command/Delete/MailDomain.php +++ b/src/HestiaCP/Command/Delete/MailDomain.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/User.php b/src/HestiaCP/Command/Delete/User.php index 1980592..702db22 100644 --- a/src/HestiaCP/Command/Delete/User.php +++ b/src/HestiaCP/Command/Delete/User.php @@ -1,13 +1,14 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/UserBackup.php b/src/HestiaCP/Command/Delete/UserBackup.php index f382cf1..c1bf5b5 100644 --- a/src/HestiaCP/Command/Delete/UserBackup.php +++ b/src/HestiaCP/Command/Delete/UserBackup.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/UserBackupExclusions.php b/src/HestiaCP/Command/Delete/UserBackupExclusions.php index 5942b84..fd15145 100644 --- a/src/HestiaCP/Command/Delete/UserBackupExclusions.php +++ b/src/HestiaCP/Command/Delete/UserBackupExclusions.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Delete/WebDomainFtp.php b/src/HestiaCP/Command/Delete/WebDomainFtp.php index 029e1a7..22b1765 100644 --- a/src/HestiaCP/Command/Delete/WebDomainFtp.php +++ b/src/HestiaCP/Command/Delete/WebDomainFtp.php @@ -1,19 +1,18 @@ user = $user; diff --git a/src/HestiaCP/Command/ICommand.php b/src/HestiaCP/Command/ICommand.php index 815c881..3a2ceae 100644 --- a/src/HestiaCP/Command/ICommand.php +++ b/src/HestiaCP/Command/ICommand.php @@ -1,5 +1,7 @@ getResponseText()); diff --git a/src/HestiaCP/Command/Lists/Database.php b/src/HestiaCP/Command/Lists/Database.php index 67b1369..14a56b2 100644 --- a/src/HestiaCP/Command/Lists/Database.php +++ b/src/HestiaCP/Command/Lists/Database.php @@ -1,16 +1,16 @@ username = $username; diff --git a/src/HestiaCP/Command/Lists/Databases.php b/src/HestiaCP/Command/Lists/Databases.php index 94cc2b7..660939d 100644 --- a/src/HestiaCP/Command/Lists/Databases.php +++ b/src/HestiaCP/Command/Lists/Databases.php @@ -1,13 +1,14 @@ username = $username; diff --git a/src/HestiaCP/Command/Lists/MailAccounts.php b/src/HestiaCP/Command/Lists/MailAccounts.php index da83440..b121570 100644 --- a/src/HestiaCP/Command/Lists/MailAccounts.php +++ b/src/HestiaCP/Command/Lists/MailAccounts.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/MailDomainDkim.php b/src/HestiaCP/Command/Lists/MailDomainDkim.php index 26f92ff..e0c606b 100644 --- a/src/HestiaCP/Command/Lists/MailDomainDkim.php +++ b/src/HestiaCP/Command/Lists/MailDomainDkim.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php index 020063e..1c86563 100644 --- a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php +++ b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/MailDomains.php b/src/HestiaCP/Command/Lists/MailDomains.php index d0d0c16..d990733 100644 --- a/src/HestiaCP/Command/Lists/MailDomains.php +++ b/src/HestiaCP/Command/Lists/MailDomains.php @@ -1,13 +1,14 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/User.php b/src/HestiaCP/Command/Lists/User.php index 4ab0c94..5c19ff9 100644 --- a/src/HestiaCP/Command/Lists/User.php +++ b/src/HestiaCP/Command/Lists/User.php @@ -1,13 +1,14 @@ username = $username; diff --git a/src/HestiaCP/Command/Lists/UserBackup.php b/src/HestiaCP/Command/Lists/UserBackup.php index 36095e3..a259de0 100644 --- a/src/HestiaCP/Command/Lists/UserBackup.php +++ b/src/HestiaCP/Command/Lists/UserBackup.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/UserBackupExclusions.php b/src/HestiaCP/Command/Lists/UserBackupExclusions.php index b0724a1..910191e 100644 --- a/src/HestiaCP/Command/Lists/UserBackupExclusions.php +++ b/src/HestiaCP/Command/Lists/UserBackupExclusions.php @@ -1,13 +1,14 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/UserBackups.php b/src/HestiaCP/Command/Lists/UserBackups.php index 908749d..56d86df 100644 --- a/src/HestiaCP/Command/Lists/UserBackups.php +++ b/src/HestiaCP/Command/Lists/UserBackups.php @@ -1,13 +1,14 @@ user = $user; diff --git a/src/HestiaCP/Command/Lists/Users.php b/src/HestiaCP/Command/Lists/Users.php index d8416a8..de0e84f 100644 --- a/src/HestiaCP/Command/Lists/Users.php +++ b/src/HestiaCP/Command/Lists/Users.php @@ -1,5 +1,7 @@ user = $user; diff --git a/src/HestiaCP/Command/ProcessCommand.php b/src/HestiaCP/Command/ProcessCommand.php index ba2ae32..af4cbfd 100644 --- a/src/HestiaCP/Command/ProcessCommand.php +++ b/src/HestiaCP/Command/ProcessCommand.php @@ -1,5 +1,7 @@ 'Command has been successfully performed', self::E_ARGS => 'Not enough arguments provided', self::E_INVALID => 'Object or argument is not valid', diff --git a/src/HestiaCP/Command/Suspend/MailAccount.php b/src/HestiaCP/Command/Suspend/MailAccount.php index c1e4152..d842fd7 100644 --- a/src/HestiaCP/Command/Suspend/MailAccount.php +++ b/src/HestiaCP/Command/Suspend/MailAccount.php @@ -1,19 +1,18 @@ user = $user; diff --git a/src/HestiaCP/Command/Suspend/MailDomain.php b/src/HestiaCP/Command/Suspend/MailDomain.php index 39f0a89..60a45fe 100644 --- a/src/HestiaCP/Command/Suspend/MailDomain.php +++ b/src/HestiaCP/Command/Suspend/MailDomain.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Suspend/User.php b/src/HestiaCP/Command/Suspend/User.php index 35615dd..83075ed 100644 --- a/src/HestiaCP/Command/Suspend/User.php +++ b/src/HestiaCP/Command/Suspend/User.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Suspend/WebDomain.php b/src/HestiaCP/Command/Suspend/WebDomain.php index 3d7b64c..b74498f 100644 --- a/src/HestiaCP/Command/Suspend/WebDomain.php +++ b/src/HestiaCP/Command/Suspend/WebDomain.php @@ -1,19 +1,18 @@ user = $user; diff --git a/src/HestiaCP/Command/TestAuthorization.php b/src/HestiaCP/Command/TestAuthorization.php index 30cf3a9..0ce054a 100644 --- a/src/HestiaCP/Command/TestAuthorization.php +++ b/src/HestiaCP/Command/TestAuthorization.php @@ -1,5 +1,7 @@ user = $user; diff --git a/src/HestiaCP/Command/Unsuspend/MailDomain.php b/src/HestiaCP/Command/Unsuspend/MailDomain.php index 3b781f5..bbd15c5 100644 --- a/src/HestiaCP/Command/Unsuspend/MailDomain.php +++ b/src/HestiaCP/Command/Unsuspend/MailDomain.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Unsuspend/User.php b/src/HestiaCP/Command/Unsuspend/User.php index 254a988..5201fbe 100644 --- a/src/HestiaCP/Command/Unsuspend/User.php +++ b/src/HestiaCP/Command/Unsuspend/User.php @@ -1,16 +1,16 @@ user = $user; diff --git a/src/HestiaCP/Command/Unsuspend/WebDomain.php b/src/HestiaCP/Command/Unsuspend/WebDomain.php index e24578a..7164b50 100644 --- a/src/HestiaCP/Command/Unsuspend/WebDomain.php +++ b/src/HestiaCP/Command/Unsuspend/WebDomain.php @@ -1,19 +1,18 @@ user = $user; diff --git a/src/HestiaCP/Exceptions/AuthorizationException.php b/src/HestiaCP/Exceptions/AuthorizationException.php index 867f191..6c7acb5 100644 --- a/src/HestiaCP/Exceptions/AuthorizationException.php +++ b/src/HestiaCP/Exceptions/AuthorizationException.php @@ -1,5 +1,7 @@ client = $client; diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index 1604571..0c292e4 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -1,5 +1,7 @@ history = []; diff --git a/tests/clientTest.php b/tests/clientTest.php index eb40047..60bdf82 100644 --- a/tests/clientTest.php +++ b/tests/clientTest.php @@ -1,5 +1,7 @@ assertSame('', $credentials->getApiKey()); @@ -14,7 +16,7 @@ public function testApiKey() { $this->assertSame('key', $credentials->getApiKey()); } - public function testUser() { + public function testUser(): void { $credentials = new Credentials('', ''); $this->assertSame('', $credentials->getUser()); @@ -22,7 +24,7 @@ public function testUser() { $this->assertSame('test', $credentials->getUser()); } - public function testPassword() { + public function testPassword(): void { $credentials = new Credentials('', ''); $this->assertSame('', $credentials->getPassword()); @@ -30,7 +32,7 @@ public function testPassword() { $this->assertSame('test', $credentials->getPassword()); } - public function testParams() { + public function testParams(): void { $credentials = new Credentials('us', 'er'); $this->assertSame(['user' => 'us', 'password' => 'er'], $credentials->getRequestParams()); @@ -41,7 +43,7 @@ public function testParams() { $this->assertSame(['access_key' => 'access', 'secret_key' => 'secret'], $credentials_accesskey->getRequestParams()); } - public function testAccessKey() { + public function testAccessKey(): void { $credentials = Credentials::accessKey('access', 'secret'); $this->assertSame('access', $credentials->getAccessKey()); $this->assertSame('secret', $credentials->getSecretKey()); @@ -50,7 +52,7 @@ public function testAccessKey() { $this->assertNull($credentials->getPassword()); } - public function testSwitchBetweenKeys() { + public function testSwitchBetweenKeys(): void { $credentials = Credentials::accessKey('access', 'secret'); $credentials->setApiKey('key'); $this->assertNull($credentials->getAccessKey()); @@ -62,7 +64,7 @@ public function testSwitchBetweenKeys() { $this->assertSame(['access_key' => 'access', 'secret_key' => 'secret'], $credentials->getRequestParams()); } - public function testNamedConstructors() { + public function testNamedConstructors(): void { $this->assertSame(['hash' => 'key'], Credentials::legacyHash('key')->getRequestParams()); $this->assertSame(['user' => 'us', 'password' => 'er'], Credentials::password('us', 'er')->getRequestParams()); } diff --git a/tests/hostTest.php b/tests/hostTest.php index 3d1db33..5f272d6 100644 --- a/tests/hostTest.php +++ b/tests/hostTest.php @@ -1,5 +1,7 @@ assertSame($expected, $host->getHostname()); @@ -54,20 +56,20 @@ public static function invalidHostnames(): array { } #[\PHPUnit\Framework\Attributes\DataProvider('invalidHostnames')] - public function testRejectsInvalidHostname(string $hostname) { + public function testRejectsInvalidHostname(string $hostname): void { $this->expectException(InvalidArgumentException::class); new Host($hostname, new Credentials('', '')); } - public function testRejectsUnbracketedIpv6() { + public function testRejectsUnbracketedIpv6(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('must be in brackets'); new Host('https://2001:db8::1', new Credentials('', '')); } - public function testSetHostnameKeepsPreviousValueOnError() { + public function testSetHostnameKeepsPreviousValueOnError(): void { $host = new Host('https://hestia', new Credentials('', '')); try { @@ -78,7 +80,7 @@ public function testSetHostnameKeepsPreviousValueOnError() { } } - public function testUri() { + public function testUri(): void { $credentials = new Credentials('', ''); $host = new Host('https://hestia', $credentials); @@ -88,7 +90,7 @@ public function testUri() { $this->assertSame('https://hestia:2083/api/', $host->buildUri()); } - public function testPort() { + public function testPort(): void { $credentials = new Credentials('', ''); $host = new Host('https://hestia', $credentials); @@ -98,7 +100,7 @@ public function testPort() { $this->assertSame(2023, $host->getPort()); } - public function testCredentials() { + public function testCredentials(): void { $credentials = new Credentials('', ''); $host = new Host('https://hestia', $credentials); diff --git a/tests/moduleTest.php b/tests/moduleTest.php index 11bc99d..33e72af 100644 --- a/tests/moduleTest.php +++ b/tests/moduleTest.php @@ -1,5 +1,7 @@ Date: Thu, 15 Oct 2026 16:07:35 +1030 Subject: [PATCH 18/19] Bugfix --- src/HestiaCP/Client.php | 13 ++++++++----- tests/moduleTest.php | 12 ++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index a97390f..fdffebb 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -24,7 +24,7 @@ class Client { private \GuzzleHttp\Client $guzzleClient; - /** @var array, Module> */ + /** @var array, array> */ private array $modules = []; /** @@ -152,15 +152,18 @@ private function prepareCommunication(?array $options = null): Client { * @return T */ private function loadModule(string $moduleName, ?string $param = null): Module { - if (!isset($this->modules[$moduleName])) { + // Modules bound to a user are cached per user, otherwise one user's module would be reused for another + $key = $param ?? ''; + + if (!isset($this->modules[$moduleName][$key])) { if ($param !== null) { - $this->modules[$moduleName] = new $moduleName($this, $param); + $this->modules[$moduleName][$key] = new $moduleName($this, $param); } else { - $this->modules[$moduleName] = new $moduleName($this); + $this->modules[$moduleName][$key] = new $moduleName($this); } } - return $this->modules[$moduleName]; + return $this->modules[$moduleName][$key]; } public function getModuleUser(): Users { diff --git a/tests/moduleTest.php b/tests/moduleTest.php index 33e72af..a9183a1 100644 --- a/tests/moduleTest.php +++ b/tests/moduleTest.php @@ -41,4 +41,16 @@ public function testDatabases(): void { $client = $this->createClient(); $this->assertSame(Databases::class, get_class($client->getModuleDatabase('test'))); } + + public function testModuleCachedPerUser(): void { + $client = $this->createClient(); + + $this->assertSame($client->getModuleUser(), $client->getModuleUser()); + + $this->assertSame($client->getModuleMail('alice'), $client->getModuleMail('alice')); + $this->assertNotSame($client->getModuleMail('alice'), $client->getModuleMail('bob')); + $this->assertNotSame($client->getModuleWeb('alice'), $client->getModuleWeb('bob')); + $this->assertNotSame($client->getModuleBackup('alice'), $client->getModuleBackup('bob')); + $this->assertNotSame($client->getModuleDatabase('alice'), $client->getModuleDatabase('bob')); + } } From c8ad6027358af2f349a62fc44be96e6904d377fd Mon Sep 17 00:00:00 2001 From: Neto Date: Thu, 15 Oct 2026 16:15:27 +1030 Subject: [PATCH 19/19] Updates --- CHANGELOG.md | 2 +- phpstan.neon.dist | 2 +- src/HestiaCP/Authorization/Credentials.php | 3 +++ src/HestiaCP/Client.php | 9 ++++++--- src/HestiaCP/Command/ICommand.php | 3 +++ src/HestiaCP/Command/ListCommand.php | 4 +++- src/HestiaCP/Command/ResponseCode.php | 1 + src/HestiaCP/Module/Backups.php | 6 +++--- src/HestiaCP/Module/Databases.php | 4 ++-- src/HestiaCP/Module/Mails.php | 8 ++++---- src/HestiaCP/Module/Users.php | 4 ++-- src/HestiaCP/Module/Webs.php | 2 +- tests/clientSendTest.php | 11 ++++++++++- tests/hostTest.php | 2 ++ 14 files changed, 42 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a39cf5..eecce97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ This will be released as 2.0.0. It brings the library up to date with HestiaCP 1 - `Delete\UserBackupExclusions`: optional `system` argument. - Tests for the client and command processing, using Guzzle's `MockHandler`. - GitHub Actions CI on PHP 8.2 to 8.5, plus a lowest-dependencies run on PHP 8.2. -- PHPStan (level 5) and a PHP-CS-Fixer coding-style check, run in CI. `composer check` runs them with the tests. +- PHPStan (level 6) and a PHP-CS-Fixer coding-style check, run in CI. `composer check` runs them with the tests. ### Changed diff --git a/phpstan.neon.dist b/phpstan.neon.dist index af2c0ce..bf58262 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,7 +3,7 @@ includes: - vendor/phpstan/phpstan-phpunit/rules.neon parameters: - level: 5 + level: 6 paths: - src - tests diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php index 31a9f5b..3646b19 100644 --- a/src/HestiaCP/Authorization/Credentials.php +++ b/src/HestiaCP/Authorization/Credentials.php @@ -94,6 +94,9 @@ public function getPassword(): ?string { return $this->password; } + /** + * @return array + */ public function getRequestParams(): array { if ($this->access_key !== null) { return [ diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index fdffebb..3d43172 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -18,6 +18,9 @@ use neto737\HestiaCP\Module\Users; use neto737\HestiaCP\Module\Webs; +/** + * @phpstan-type ClientOptions array{timeout?: float, verify?: bool|string, handler?: callable} + */ class Client { private Host $host; @@ -37,7 +40,7 @@ class Client { * - handler (callable) Custom Guzzle handler stack (middleware, testing) * * @param Host $host - * @param array|null $options + * @param ClientOptions|null $options * @throws ClientException */ public function __construct(Host $host, ?array $options = null) { @@ -49,7 +52,7 @@ public function __construct(Host $host, ?array $options = null) { * @param string $hostname Scheme and host, e.g. https://panel.example.com * @param string $user Username or API Key * @param string|null $password Password (must be null if using API Key) - * @param array|null $options See Client::__construct() + * @param ClientOptions|null $options See Client::__construct() * @return Client * @throws ClientException * @throws InvalidArgumentException @@ -126,7 +129,7 @@ private function getTimeout(ICommand $command): float { } /** - * @param array|null $options + * @param ClientOptions|null $options * @return Client * @throws ClientException */ diff --git a/src/HestiaCP/Command/ICommand.php b/src/HestiaCP/Command/ICommand.php index 3a2ceae..f3f13c3 100644 --- a/src/HestiaCP/Command/ICommand.php +++ b/src/HestiaCP/Command/ICommand.php @@ -21,5 +21,8 @@ public function process(): mixed; */ public function getTimeout(): ?float; + /** + * @return array + */ public function getRequestParams(): array; } diff --git a/src/HestiaCP/Command/ListCommand.php b/src/HestiaCP/Command/ListCommand.php index eb1748e..2807950 100644 --- a/src/HestiaCP/Command/ListCommand.php +++ b/src/HestiaCP/Command/ListCommand.php @@ -15,7 +15,7 @@ public function needReturnCode(): bool { } /** - * @return array + * @return array * @throws ProcessException * @throws AuthorizationException */ @@ -45,6 +45,8 @@ public function process(): array { /** * Returns the only item of a single-object list, e.g. {"admin": {...}}. * + * @param array $list + * @return array * @throws InvalidResponseException */ protected function convertDetail(array $list): array { diff --git a/src/HestiaCP/Command/ResponseCode.php b/src/HestiaCP/Command/ResponseCode.php index 1218c0e..f302057 100644 --- a/src/HestiaCP/Command/ResponseCode.php +++ b/src/HestiaCP/Command/ResponseCode.php @@ -31,6 +31,7 @@ class ResponseCode { public const E_BACKUP = 21; public const E_API_DISABLED = 21; + /** @var array */ public static array $messages = [ self::OK => 'Command has been successfully performed', self::E_ARGS => 'Not enough arguments provided', diff --git a/src/HestiaCP/Module/Backups.php b/src/HestiaCP/Module/Backups.php index b4fdc2e..35116b5 100644 --- a/src/HestiaCP/Module/Backups.php +++ b/src/HestiaCP/Module/Backups.php @@ -60,7 +60,7 @@ public function deleteExclusions(?string $system = null): bool { /** * This function for obtaining the list of available user backups. * - * @return array + * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -72,7 +72,7 @@ public function listBackups(): array { * This function of obtaining the list of backup parameters. * * @param string $backup - * @return array + * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -83,7 +83,7 @@ public function listBackup(string $backup): array { /** * This function for obtaining the backup exclusion list * - * @return array + * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php index 3aeb6a1..0697016 100644 --- a/src/HestiaCP/Module/Databases.php +++ b/src/HestiaCP/Module/Databases.php @@ -62,7 +62,7 @@ public function deleteDatabases(): bool { * This function for obtaining of all database's parameters. * * @param string $database - * @return array + * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -73,7 +73,7 @@ public function listDatabase(string $database): array { /** * This function for obtaining the list of all user's databases. * - * @return array[] + * @return array> * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ diff --git a/src/HestiaCP/Module/Mails.php b/src/HestiaCP/Module/Mails.php index e0af666..597a3c1 100644 --- a/src/HestiaCP/Module/Mails.php +++ b/src/HestiaCP/Module/Mails.php @@ -32,7 +32,7 @@ public function __construct(Client $client, string $user) { * This function of obtaining the list of all user domains. * * @param string $domain - * @return array[] + * @return array> * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -162,7 +162,7 @@ public function deleteDomain(string $domain): bool { * This function of obtaining domain dkim files. * * @param string $domain - * @return array[] + * @return array> * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -174,7 +174,7 @@ public function listDomainDkim(string $domain): array { * This function of obtaining domain dkim dns records for proper setup. * * @param string $domain - * @return array[] + * @return array> * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -185,7 +185,7 @@ public function listDomainDkimDns(string $domain): array { /** * This function of obtaining the list of all user domains. * - * @return array[] + * @return array> * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index 0c292e4..378f894 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -18,7 +18,7 @@ class Users extends Module { /** * This function to obtain the list of all system users. * - * @return array + * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ @@ -30,7 +30,7 @@ public function list(): array { * This function to obtain user parameters. * * @param string $user - * @return array + * @return array * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ diff --git a/src/HestiaCP/Module/Webs.php b/src/HestiaCP/Module/Webs.php index ffafa4c..66075d4 100644 --- a/src/HestiaCP/Module/Webs.php +++ b/src/HestiaCP/Module/Webs.php @@ -168,7 +168,7 @@ public function deleteDomainFtp(string $domain, string $ftpUser): bool { /** * This function to obtain the list of all user web domains. * - * @return array[] + * @return array> * @throws \neto737\HestiaCP\Exceptions\ClientException * @throws \neto737\HestiaCP\Exceptions\ProcessException */ diff --git a/tests/clientSendTest.php b/tests/clientSendTest.php index 4c5c780..464da43 100644 --- a/tests/clientSendTest.php +++ b/tests/clientSendTest.php @@ -20,18 +20,27 @@ use neto737\HestiaCP\Command\ResponseCode; use neto737\HestiaCP\Exceptions\InvalidResponseException; use neto737\HestiaCP\Exceptions\ProcessException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; require_once __DIR__ . '/../vendor/autoload.php'; /** * Responses mimic HestiaCP's web/api/index.php: the access key endpoint sets the * Hestia-Exit-Code header and an HTTP error status, the legacy endpoint only echoes the code. + * + * @phpstan-import-type ClientOptions from Client + * @phpstan-type HistoryEntry array{request: RequestInterface, response: ResponseInterface|null, error: mixed, options: array} */ class clientSendTest extends \PHPUnit\Framework\TestCase { - /** @var array */ + /** @var array */ private array $history = []; + /** + * @param list $responses + * @param ClientOptions $options + */ private function createClient(array $responses, array $options = []): Client { $this->history = []; $stack = HandlerStack::create(new MockHandler($responses)); diff --git a/tests/hostTest.php b/tests/hostTest.php index 5f272d6..01bed59 100644 --- a/tests/hostTest.php +++ b/tests/hostTest.php @@ -20,6 +20,7 @@ public function testHost(): void { $this->assertSame('http://host', $host->getHostname()); } + /** @return array */ public static function normalizedHostnames(): array { return [ 'trailing slash' => ['https://hestia/', 'https://hestia'], @@ -38,6 +39,7 @@ public function testNormalizesHostname(string $hostname, string $expected): void $this->assertSame($expected, $host->getHostname()); } + /** @return array */ public static function invalidHostnames(): array { return [ 'empty' => [''],