diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..abb4411
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +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
+/.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/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/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..6221c79
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,69 @@
+name: Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ 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
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['8.2', '8.3', '8.4', '8.5']
+ dependencies: [highest]
+ include:
+ - php: '8.2'
+ 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..fc012d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,8 @@
composer.lock
test.php
/vendor
-/tests/log
\ No newline at end of file
+/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..06e6c15
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,28 @@
+in([__DIR__ . '/src', __DIR__ . '/tests']);
+
+return (new PhpCsFixer\Config())
+ ->setRiskyAllowed(true)
+ ->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,
+ 'declare_strict_types' => true,
+ ])
+ ->setFinder($finder);
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/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..eecce97
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,85 @@
+# 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.2 to 8.5, plus a lowest-dependencies run on PHP 8.2.
+- PHPStan (level 6) and a PHP-CS-Fixer coding-style check, run in CI. `composer check` runs them with the tests.
+
+### Changed
+
+- **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.
+- **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 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.
+- `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 `^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
+
+- **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/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 b3fe0be..1c4ba75 100644
--- a/README.md
+++ b/README.md
@@ -1,235 +1,147 @@
+
+
+
-# HestiaCP PHP API
+HestiaCP PHP API
-
-[](//packagist.org/packages/neto737/hestiacp-api) [](https://packagist.org/packages/neto737/hestiacp-api) [](https://packagist.org/packages/neto737/hestiacp-api) [](https://packagist.org/packages/neto737/hestiacp-api) [](https://www.travis-ci.com/github/neto737/HestiaCP-API) [](https://codecov.io/gh/neto737/HestiaCP-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.
+
-
+
+
+
+
+
+
+
+
-## How to use
+---
-1) Installation
+## Features
-```sh
-
-$ composer require neto737/hestiacp-api
-
-```
+- **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.2 to 8.5 and checked against HestiaCP 1.10.
-2) Create Client
+## Requirements
-a) Easy way
+- PHP 8.2 or later
+- HestiaCP with the API enabled, and the IP address of the calling machine allowed in the server settings
-```php
-
-use neto737\HestiaCP\Client;
-
-// Easy way to create Client
-
-// Using API Key
-$client = Client::simpleFactory('https://someHost', 'API_Key');
-
-// Using username and password
-$client = Client::simpleFactory('https://someHost', 'someUser', 'somePass');
+## Installation
+```sh
+composer require neto737/hestiacp-api
```
-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 API Key or username and password
-// API Key
-$credentials = new Credentials('API_Key');
+// Access key (HestiaCP 1.6+), written as ACCESS_KEY:SECRET_KEY
+$client = Client::simpleFactory('https://panel.example.com', 'ACCESS_KEY:SECRET_KEY');
-// Username and Password
-$credentials = new Credentials('someUser', 'somePassword');
-
-$host = new Host('https://someHost', $credentials);
-
-$client = new Client($host);
-
-```
-
-3) Usage
-
-```php
-
-// verify login
+// Check that the credentials work
$client->testAuthorization(); // bool
-```
-
-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
-
-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->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->deleteDomainLetsEncrypt('domain.com');
-
-$webModule->addDomainFtp('domain.com', 'test', 'pa$$word');
+// List all users
+$users = $client->getModuleUser()->list(); // ['admin' => ['NAME' => '...', 'CONTACT' => '...', ...], ...]
-$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');
+// 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');
```
-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
+> [!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).
-$mailModule->deleteAccount('domain.com', 'info');
+## Modules
-$mailModule->suspendDomain('domain.com');
+| 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. |
-$mailModule->unsuspendDomain('domain.com');
+Need a command that isn't wrapped in a module? See [Sending commands directly](docs/commands.md).
-$mailModule->deleteDomain('domain.com');
-
-```
-
-d) db module
+## Error handling
```php
-
-$dbModule = $client->getModuleDatabase('admin');
-
-
-$dbModule->add('database', 'dbuser', 'dbpass');
-
-$dbModule->delete('admin_database');
-
-$dbModule->deleteDatabases();
-
-$dbModule->listDatabase('database');
-
-$dbModule->listDatabases();
-
-
-// todo
-
-// ... etc
-
+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, ...
+}
```
-e) cron module
+All exception types and exit codes are listed in [Error handling](docs/errors.md).
-```php
+## Documentation
-$cronModule = $client->getModuleCron();
+- [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
+**Upgrading from v1?** Read [UPGRADING.md](UPGRADING.md). All changes are listed in [CHANGELOG.md](CHANGELOG.md).
-// todo
+## Contributing
-// ... etc
+Bug reports and pull requests are welcome. To run the checks:
+```sh
+composer install
+composer check # coding style, static analysis and tests
```
-f) backup module
-
-```php
-
-$backupModule = $client->getModuleBackup('admin'); // backup module needs user
-
-$backupModule->backup(); // create a new backup
-
-$backupModule->delete('admin.2021-10-13_18-12-53.tar'); // delete an user backup
+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.
-$backupModule->deleteExclusions(); // delete all backup exclusions
+The tests use Guzzle's mock handler, so they don't need a HestiaCP server.
-$backupModule->listBackups(); // returns the backups list
+## Credits
-$backupModule->listBackup('admin.2021-10-13_18-12-53.tar'); // returns backup parameters list
+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.
-$backupModule->listBackupExclusions(); // returns the backup exclusions list
+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:
-## Donate :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/UPGRADING.md b/UPGRADING.md
new file mode 100644
index 0000000..dafe93e
--- /dev/null
+++ b/UPGRADING.md
@@ -0,0 +1,128 @@
+# 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.2 or later
+
+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.
+
+### 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.
+- `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).
+
+### 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/composer.json b/composer.json
index 73b0f45..0853854 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": ">=7.3",
- "guzzlehttp/guzzle": "^7.4",
- "nette/utils": "^3.2"
+ "php": "^8.2",
+ "guzzlehttp/guzzle": "^7.15.2 || ^8.2",
+ "psr/http-message": "^1.1 || ^2.0"
},
"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": [
{
@@ -14,11 +40,37 @@
"email": "neto737@live.com"
}
],
- "minimum-stability": "dev",
+ "minimum-stability": "stable",
"autoload": {
- "classmap": ["src/"]
+ "psr-4": {
+ "neto737\\HestiaCP\\": "src/HestiaCP/"
+ }
},
"require-dev": {
- "phpunit/phpunit": "^9"
+ "friendsofphp/php-cs-fixer": "^3.86",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^11.5 || ^12.0"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "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/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)).
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..bf58262
--- /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: 6
+ paths:
+ - src
+ - tests
+ tmpDir: .phpstan.cache
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
+
+
+
diff --git a/src/HestiaCP/Authorization/Credentials.php b/src/HestiaCP/Authorization/Credentials.php
index 3feb43a..3646b19 100644
--- a/src/HestiaCP/Authorization/Credentials.php
+++ b/src/HestiaCP/Authorization/Credentials.php
@@ -1,23 +1,26 @@
setApiKey($user);
} else {
@@ -26,40 +29,88 @@ public function __construct(string $user, string $password = null) {
}
}
- public function setApiKey(string $key) {
+ /**
+ * 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): static {
+ $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): static {
$this->api_key = $key;
+ $this->access_key = null;
+ $this->secret_key = null;
return $this;
}
- public function getApiKey(): string {
+ public function getApiKey(): ?string {
return $this->api_key;
}
- public function setUser(string $user) {
+ public function setUser(string $user): static {
$this->user = $user;
return $this;
}
- public function getUser(): string {
+ public function getUser(): ?string {
return $this->user;
}
- public function setPassword(string $password) {
+ public function setPassword(string $password): static {
$this->password = $password;
return $this;
}
- public function getPassword(): string {
+ public function getPassword(): ?string {
return $this->password;
}
+ /**
+ * @return array
+ */
public function getRequestParams(): array {
- if ($this->api_key) {
+ 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/src/HestiaCP/Authorization/Host.php b/src/HestiaCP/Authorization/Host.php
index f636b0c..d55c529 100644
--- a/src/HestiaCP/Authorization/Host.php
+++ b/src/HestiaCP/Authorization/Host.php
@@ -1,26 +1,39 @@
setHostname($hostname);
$this->setCredentials($credentials);
$this->setPort($port);
}
- public function setHostname(string $hostname) {
- $this->hostname = $hostname;
+ /**
+ * 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): static {
+ $this->hostname = self::normalizeHostname($hostname);
return $this;
}
@@ -28,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;
}
@@ -37,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;
}
@@ -49,4 +62,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 e080417..3d43172 100644
--- a/src/HestiaCP/Client.php
+++ b/src/HestiaCP/Client.php
@@ -1,55 +1,70 @@
, array> */
+ private array $modules = [];
/**
* 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
+ * @param ClientOptions|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);
}
/**
- * @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 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 ClientOptions|null $options See Client::__construct()
* @return Client
* @throws ClientException
+ * @throws InvalidArgumentException
*/
- 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) {
+ public function setHost(Host $host): static {
$this->host = $host;
return $this;
}
@@ -74,13 +89,9 @@ public function testAuthorization(): bool {
* @throws ClientException
* @throws ProcessException
*/
- public function send(ICommand $command) {
+ public function send(ICommand $command): mixed {
$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,29 +101,46 @@ 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->getTimeout();
+
+ if ($commandTimeout === null || $timeout == 0) {
+ return $timeout;
}
+
+ return $commandTimeout == 0 ? 0 : max($timeout, $commandTimeout);
}
/**
- * @param array|null $options
+ * @param ClientOptions|null $options
* @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
+ '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() . ')');
@@ -121,16 +149,24 @@ private function prepareCommunication(array $options = null): Client {
return $this;
}
- private function loadModule($moduleName, $param = null) {
- if (!isset($this->modules[$moduleName])) {
+ /**
+ * @template T of Module
+ * @param class-string $moduleName
+ * @return T
+ */
+ private function loadModule(string $moduleName, ?string $param = null): Module {
+ // 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/src/HestiaCP/Command/Add/Database.php b/src/HestiaCP/Command/Add/Database.php
index 2945fb1..cf2463f 100644
--- a/src/HestiaCP/Command/Add/Database.php
+++ b/src/HestiaCP/Command/Add/Database.php
@@ -1,32 +1,29 @@
user = $user;
$this->database = $database;
$this->dbuser = $dbuser;
$this->dbpass = $dbpass;
- $this->type = $type;
+ $this->type = $type;
}
public function getName(): string {
@@ -39,7 +36,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/Add/LetsEncryptDomain.php b/src/HestiaCP/Command/Add/LetsEncryptDomain.php
index 8ffea33..0af7121 100644
--- a/src/HestiaCP/Command/Add/LetsEncryptDomain.php
+++ b/src/HestiaCP/Command/Add/LetsEncryptDomain.php
@@ -1,40 +1,71 @@
user = $user;
$this->domain = $domain;
$this->aliases = $aliases;
- $this->restart = $restart;
+ $this->mail = $mail;
+ $this->keyAlgo = $keyAlgo;
}
public function getName(): string {
return 'v-add-letsencrypt-domain';
}
+ public function getTimeout(): ?float {
+ return 90;
+ }
+
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/MailAccount.php b/src/HestiaCP/Command/Add/MailAccount.php
index b029093..54b3b23 100644
--- a/src/HestiaCP/Command/Add/MailAccount.php
+++ b/src/HestiaCP/Command/Add/MailAccount.php
@@ -1,22 +1,20 @@
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 cd153c2..bd5acb9 100644
--- a/src/HestiaCP/Command/Add/User.php
+++ b/src/HestiaCP/Command/Add/User.php
@@ -1,28 +1,32 @@
user = $user;
$this->password = $password;
$this->email = $email;
$this->package = $package;
+ $this->name = $name;
+ $this->lastname = $lastname;
}
public function getName(): string {
@@ -30,11 +34,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/Add/WebDomain.php b/src/HestiaCP/Command/Add/WebDomain.php
index f9ee595..b6ce460 100644
--- a/src/HestiaCP/Command/Add/WebDomain.php
+++ b/src/HestiaCP/Command/Add/WebDomain.php
@@ -1,30 +1,26 @@
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..800396a 100644
--- a/src/HestiaCP/Command/Add/WebDomainFtp.php
+++ b/src/HestiaCP/Command/Add/WebDomainFtp.php
@@ -1,27 +1,24 @@
user = $user;
$this->domain = $domain;
$this->ftpUser = $ftpUser;
diff --git a/src/HestiaCP/Command/Backup/User.php b/src/HestiaCP/Command/Backup/User.php
index 93b5d23..00bcdc4 100644
--- a/src/HestiaCP/Command/Backup/User.php
+++ b/src/HestiaCP/Command/Backup/User.php
@@ -1,30 +1,35 @@
user = $user;
+ $this->notify = $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 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)
- ];
- }
+ public function getRequestParams(): array {
+ return [
+ self::ARG_1 => $this->user,
+ self::ARG_2 => $this->convertBool($this->notify)
+ ];
+ }
}
diff --git a/src/HestiaCP/Command/Backup/Users.php b/src/HestiaCP/Command/Backup/Users.php
index 9ea8644..70f5736 100644
--- a/src/HestiaCP/Command/Backup/Users.php
+++ b/src/HestiaCP/Command/Backup/Users.php
@@ -1,18 +1,23 @@
self::FORMAT_JSON
- ];
+ return [];
}
}
diff --git a/src/HestiaCP/Command/Change/MailAccountPassword.php b/src/HestiaCP/Command/Change/MailAccountPassword.php
index 4f11ee2..9776181 100644
--- a/src/HestiaCP/Command/Change/MailAccountPassword.php
+++ b/src/HestiaCP/Command/Change/MailAccountPassword.php
@@ -1,22 +1,20 @@
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 39088c6..1b2e2bd 100644
--- a/src/HestiaCP/Command/Command.php
+++ b/src/HestiaCP/Command/Command.php
@@ -1,11 +1,12 @@
getMessage());
- }
+ private ?int $lastStatusCode = null;
+
+ private ?int $lastExitCode = null;
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 +69,42 @@ public function getResponseCode(): int {
/**
* @throws AuthorizationException
+ * @throws ProcessException
*/
public function defaultProcess(): void {
- $responseText = $this->getResponseText();
+ $responseText = (string) $this->getResponseText();
+
+ if (strpos($responseText, 'Error: authentication failed') === 0) {
+ throw new AuthorizationException('Authorization failed! Bad user or password', ResponseCode::E_PASSWORD);
+ }
- if ($responseText === 'Error: authentication failed') {
- throw new AuthorizationException('Authorization failed! Bad user or 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
- * @throws ProcessException
+ * Builds the exception for a non-zero HestiaCP exit code.
*/
- protected function throwCodeException($code): void {
- throw new ProcessException(ResponseCode::$messages[$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_FORBIDDEN) {
+ return new AuthorizationException($message, $code);
+ }
+
+ return new ProcessException($message, $code);
}
/**
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 b1dac8d..c1bf5b5 100644
--- a/src/HestiaCP/Command/Delete/UserBackup.php
+++ b/src/HestiaCP/Command/Delete/UserBackup.php
@@ -1,20 +1,20 @@
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/Delete/UserBackupExclusions.php b/src/HestiaCP/Command/Delete/UserBackupExclusions.php
index 81e20c6..fd15145 100644
--- a/src/HestiaCP/Command/Delete/UserBackupExclusions.php
+++ b/src/HestiaCP/Command/Delete/UserBackupExclusions.php
@@ -1,22 +1,24 @@
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/Command/Delete/WebDomain.php b/src/HestiaCP/Command/Delete/WebDomain.php
index 6e21fbc..dc04cb3 100644
--- a/src/HestiaCP/Command/Delete/WebDomain.php
+++ b/src/HestiaCP/Command/Delete/WebDomain.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 a8924ab..f3f13c3 100644
--- a/src/HestiaCP/Command/ICommand.php
+++ b/src/HestiaCP/Command/ICommand.php
@@ -1,8 +1,9 @@
+ */
public function getRequestParams(): array;
}
diff --git a/src/HestiaCP/Command/ListCommand.php b/src/HestiaCP/Command/ListCommand.php
index 732c46e..2807950 100644
--- a/src/HestiaCP/Command/ListCommand.php
+++ b/src/HestiaCP/Command/ListCommand.php
@@ -1,12 +1,12 @@
* @throws ProcessException
* @throws AuthorizationException
*/
- public function process() {
+ public function process(): array {
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) {
+ $response = json_decode($responseJson, true, 512, JSON_THROW_ON_ERROR);
+ } catch (\JsonException $e) {
+ throw new ProcessException($responseJson, 0, $e);
+ }
+
+ if (!is_array($response)) {
throw new ProcessException($responseJson);
}
- // convert response to array
- return $this->convertList((array)$response);
+ return $response;
}
- protected function convertList(array $list): array {
- foreach ($list as $key => $item) {
- $list[$key] = ArrayHash::from((array) $item);
- }
+ /**
+ * 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 {
+ $detail = array_shift($list);
- return $list;
- }
+ if (!is_array($detail)) {
+ throw new InvalidResponseException('Empty response');
+ }
- protected function convertDetail(array $list): ArrayHash {
- return array_shift($list);
+ return $detail;
}
}
diff --git a/src/HestiaCP/Command/Lists/Database.php b/src/HestiaCP/Command/Lists/Database.php
index dfd7c03..14a56b2 100644
--- a/src/HestiaCP/Command/Lists/Database.php
+++ b/src/HestiaCP/Command/Lists/Database.php
@@ -1,35 +1,34 @@
username = $username;
- $this->database = $database;
+ $this->database = $database;
}
public function getName(): string {
return 'v-list-database';
}
- public function process(): ArrayHash {
+ public function process(): array {
return $this->convertDetail(parent::process());
}
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/Databases.php b/src/HestiaCP/Command/Lists/Databases.php
index 127bb28..660939d 100644
--- a/src/HestiaCP/Command/Lists/Databases.php
+++ b/src/HestiaCP/Command/Lists/Databases.php
@@ -1,14 +1,14 @@
username = $username;
@@ -18,10 +18,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/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 3758442..e0c606b 100644
--- a/src/HestiaCP/Command/Lists/MailDomainDkim.php
+++ b/src/HestiaCP/Command/Lists/MailDomainDkim.php
@@ -1,16 +1,16 @@
user = $user;
@@ -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*\}~', '}', 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 bd19ce3..1c86563 100644
--- a/src/HestiaCP/Command/Lists/MailDomainDkimDns.php
+++ b/src/HestiaCP/Command/Lists/MailDomainDkimDns.php
@@ -1,16 +1,16 @@
user = $user;
@@ -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~', '', parent::getResponseText());
- }
-
public function getRequestParams(): array {
return [
self::ARG_1 => $this->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 07ee07a..5c19ff9 100644
--- a/src/HestiaCP/Command/Lists/User.php
+++ b/src/HestiaCP/Command/Lists/User.php
@@ -1,14 +1,14 @@
username = $username;
@@ -18,7 +18,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..a259de0 100644
--- a/src/HestiaCP/Command/Lists/UserBackup.php
+++ b/src/HestiaCP/Command/Lists/UserBackup.php
@@ -1,36 +1,35 @@
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(): ArrayHash {
+ 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..910191e 100644
--- a/src/HestiaCP/Command/Lists/UserBackupExclusions.php
+++ b/src/HestiaCP/Command/Lists/UserBackupExclusions.php
@@ -1,26 +1,27 @@
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..56d86df 100644
--- a/src/HestiaCP/Command/Lists/UserBackups.php
+++ b/src/HestiaCP/Command/Lists/UserBackups.php
@@ -1,26 +1,27 @@
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/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 cea4322..af4cbfd 100644
--- a/src/HestiaCP/Command/ProcessCommand.php
+++ b/src/HestiaCP/Command/ProcessCommand.php
@@ -1,10 +1,11 @@
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..f302057 100644
--- a/src/HestiaCP/Command/ResponseCode.php
+++ b/src/HestiaCP/Command/ResponseCode.php
@@ -1,5 +1,7 @@
'Command has been successfuly performed',
+ /** @var array */
+ public static array $messages = [
+ 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 +43,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_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',
- 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/Suspend/MailAccount.php b/src/HestiaCP/Command/Suspend/MailAccount.php
index 9f6a839..d842fd7 100644
--- a/src/HestiaCP/Command/Suspend/MailAccount.php
+++ b/src/HestiaCP/Command/Suspend/MailAccount.php
@@ -1,35 +1,34 @@
user = $user;
- $this->domain = $domain;
- $this->account = $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 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
- ];
- }
+ 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..60a45fe 100644
--- a/src/HestiaCP/Command/Suspend/MailDomain.php
+++ b/src/HestiaCP/Command/Suspend/MailDomain.php
@@ -1,30 +1,30 @@
user = $user;
- $this->domain = $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 getName(): string {
+ return 'v-suspend-mail-domain';
+ }
- public function getRequestParams(): array {
- return [
- self::ARG_1 => $this->user,
- self::ARG_2 => $this->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..83075ed 100644
--- a/src/HestiaCP/Command/Suspend/User.php
+++ b/src/HestiaCP/Command/Suspend/User.php
@@ -1,30 +1,30 @@
user = $user;
- $this->restart = $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 getName(): string {
+ return 'v-suspend-user';
+ }
- public function getRequestParams(): array {
- return [
- self::ARG_1 => $this->user,
- self::ARG_2 => $this->convertBool($this->restart)
- ];
- }
+ 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..b74498f 100644
--- a/src/HestiaCP/Command/Suspend/WebDomain.php
+++ b/src/HestiaCP/Command/Suspend/WebDomain.php
@@ -1,35 +1,34 @@
user = $user;
- $this->domain = $domain;
- $this->restart = $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 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)
- ];
- }
+ 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/TestAuthorization.php b/src/HestiaCP/Command/TestAuthorization.php
index 0f61bb1..0ce054a 100644
--- a/src/HestiaCP/Command/TestAuthorization.php
+++ b/src/HestiaCP/Command/TestAuthorization.php
@@ -1,15 +1,21 @@
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/Command/Unsuspend/MailAccount.php b/src/HestiaCP/Command/Unsuspend/MailAccount.php
index 685d93c..651e214 100644
--- a/src/HestiaCP/Command/Unsuspend/MailAccount.php
+++ b/src/HestiaCP/Command/Unsuspend/MailAccount.php
@@ -1,35 +1,34 @@
user = $user;
- $this->domain = $domain;
- $this->account = $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 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
- ];
- }
+ 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..bbd15c5 100644
--- a/src/HestiaCP/Command/Unsuspend/MailDomain.php
+++ b/src/HestiaCP/Command/Unsuspend/MailDomain.php
@@ -1,30 +1,30 @@
user = $user;
- $this->domain = $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 getName(): string {
+ return 'v-unsuspend-mail-domain';
+ }
- public function getRequestParams(): array {
- return [
- self::ARG_1 => $this->user,
- self::ARG_2 => $this->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..5201fbe 100644
--- a/src/HestiaCP/Command/Unsuspend/User.php
+++ b/src/HestiaCP/Command/Unsuspend/User.php
@@ -1,30 +1,30 @@
user = $user;
- $this->restart = $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 getName(): string {
+ return 'v-unsuspend-user';
+ }
- public function getRequestParams(): array {
- return [
- self::ARG_1 => $this->user,
- self::ARG_2 => $this->convertBool($this->restart)
- ];
- }
+ 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..7164b50 100644
--- a/src/HestiaCP/Command/Unsuspend/WebDomain.php
+++ b/src/HestiaCP/Command/Unsuspend/WebDomain.php
@@ -1,35 +1,34 @@
user = $user;
- $this->domain = $domain;
- $this->restart = $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 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)
- ];
- }
+ 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.php b/src/HestiaCP/Exceptions.php
deleted file mode 100644
index 91b5dd2..0000000
--- a/src/HestiaCP/Exceptions.php
+++ /dev/null
@@ -1,17 +0,0 @@
-user = $user;
}
- /**
- * This call is used for backing up user with all its domains and databases.
- *
- * @param bool $notify
- * @return string
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function backup(bool $notify = true): string {
- return $this->client->send(new BackupUser($this->user, $notify));
- }
+ /**
+ * 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));
+ }
- /**
- * This function deletes user backup.
- *
- * @param string $backup
- * @return bool
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function delete(string $backup): bool {
- return $this->client->send(new DeleteUserBackup($this->user, $backup));
- }
+ /**
+ * 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));
+ }
- /**
- * This function for deleting backup exclusion
- *
- * @return bool
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function deleteExclusions(): bool {
- return $this->client->send(new DeleteUserBackupExclusions($this->user));
- }
+ /**
+ * 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));
+ }
- /**
- * This function for obtaining the list of available user backups.
- *
- * @return array
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function listBackups(): array {
- return $this->client->send(new UserBackups($this->user));
- }
+ /**
+ * 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));
+ }
- /**
- * This function of obtaining the list of backup parameters.
- *
- * @param string $backup
- * @return ArrayHash
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function listBackup(string $backup): ArrayHash {
- return $this->client->send(new ListsUserBackup($this->user, $backup));
- }
+ /**
+ * 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));
+ }
- /**
- * This function for obtaining the backup exclusion list
- *
- * @return array
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function listBackupExclusions(): array {
- return $this->client->send(new ListsUserBackupExclusions($this->user));
- }
-}
\ No newline at end of file
+ /**
+ * 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));
+ }
+}
diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php
index 9891b6a..0697016 100644
--- a/src/HestiaCP/Module/Databases.php
+++ b/src/HestiaCP/Module/Databases.php
@@ -1,10 +1,10 @@
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\ClientException
- * @throws \neto737\HestiaCP\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));
- }
+ /**
+ * 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));
+ }
- /**
- * This function is used for delete a specified database.
- *
- * @param string $database
- * @return bool
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function delete(string $database): bool {
- return $this->client->send(new DeleteDatabase($this->user, $database));
- }
+ /**
+ * 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));
+ }
- /**
- * This function deletes all user databases.
- *
- * @return bool
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function deleteDatabases(): bool {
- return $this->client->send(new DeleteDatabases($this->user));
- }
+ /**
+ * 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));
+ }
- /**
- * This function for obtaining of all database's parameters.
- *
- * @param string $database
- * @return ArrayHash
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function listDatabase(string $database): ArrayHash {
- return $this->client->send(new ListDatabase($this->user, $database));
- }
+ /**
+ * 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));
+ }
- /**
- * This function for obtaining the list of all user's databases.
- *
- * @param string $database
- * @return ArrayHash
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
- */
- public function listDatabases(): ArrayHash {
- return $this->client->send(new ListDatabases($this->user));
- }
-}
\ No newline at end of file
+ /**
+ * 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));
+ }
+}
diff --git a/src/HestiaCP/Module/IModule.php b/src/HestiaCP/Module/IModule.php
index 4520c52..50519f8 100644
--- a/src/HestiaCP/Module/IModule.php
+++ b/src/HestiaCP/Module/IModule.php
@@ -1,5 +1,7 @@
>
+ * @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));
@@ -42,13 +42,13 @@ public function listAccounts(string $domain): array {
/**
* This function add new email account.
- *
+ *
* @param string $domain
* @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));
@@ -56,13 +56,13 @@ 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
* @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));
@@ -70,38 +70,38 @@ public function changeAccountPassword(string $domain, string $account, string $p
/**
* This function suspends mail account.
- *
+ *
* @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));
+ return $this->client->send(new SuspendMailAccount($this->user, $domain, $account));
}
/**
* This function unsuspends mail account.
- *
+ *
* @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));
+ return $this->client->send(new UnsuspendMailAccount($this->user, $domain, $account));
}
/**
* This function deletes email account.
- *
+ *
* @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));
@@ -109,14 +109,14 @@ public function deleteAccount(string $domain, string $account): bool {
/**
* This function adds MAIL domain.
- *
+ *
* @param string $domain
* @param bool $antispam
* @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));
@@ -124,11 +124,11 @@ public function addDomain(string $domain, bool $antispam = true, bool $antivirus
/**
* This function suspends mail domain.
- *
+ *
* @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));
@@ -136,11 +136,11 @@ public function suspendDomain(string $domain): bool {
/**
* This function unsuspends mail domain.
- *
+ *
* @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));
@@ -148,11 +148,11 @@ 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\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));
@@ -160,11 +160,11 @@ public function deleteDomain(string $domain): bool {
/**
* This function of obtaining domain dkim files.
- *
+ *
* @param string $domain
- * @return ArrayHash[]
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ * @return array>
+ * @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));
@@ -172,11 +172,11 @@ public function listDomainDkim(string $domain): array {
/**
* This function of obtaining domain dkim dns records for proper setup.
- *
+ *
* @param string $domain
- * @return ArrayHash[]
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ * @return array>
+ * @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));
@@ -184,10 +184,10 @@ public function listDomainDkimDns(string $domain): array {
/**
* This function of obtaining the list of all user domains.
- *
- * @return ArrayHash[]
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ *
+ * @return array>
+ * @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/Module.php b/src/HestiaCP/Module/Module.php
index 2f10656..5be683a 100644
--- a/src/HestiaCP/Module/Module.php
+++ b/src/HestiaCP/Module/Module.php
@@ -1,13 +1,14 @@
client = $client;
diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php
index 6316228..378f894 100644
--- a/src/HestiaCP/Module/Users.php
+++ b/src/HestiaCP/Module/Users.php
@@ -1,12 +1,13 @@
+ * @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 ArrayHash
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ * @return array
+ * @throws \neto737\HestiaCP\Exceptions\ClientException
+ * @throws \neto737\HestiaCP\Exceptions\ProcessException
*/
- public function detail(string $user): ArrayHash {
+ public function detail(string $user): array {
return $this->client->send(new User($user));
}
/**
* This function changes user's password and updates RKEY value.
- *
+ *
* @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));
@@ -52,27 +53,29 @@ public function changePassword(string $user, string $password): bool {
/**
* This function creates new user account.
- *
+ *
* @param string $user
* @param string $password
* @param string $email
* @param string|null $package
+ * @param string|null $name
+ * @param string|null $lastname
* @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 {
- 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));
}
/**
* This function suspends a certain user and all his objects.
- *
+ *
* @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));
@@ -80,12 +83,12 @@ 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
- * @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));
@@ -93,11 +96,11 @@ 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\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));
@@ -105,12 +108,12 @@ public function delete(string $user): bool {
/**
* This function backups all system users.
- *
- * @return string
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ *
+ * @return bool
+ * @throws \neto737\HestiaCP\Exceptions\ClientException
+ * @throws \neto737\HestiaCP\Exceptions\ProcessException
*/
- public function backup(): string {
- return $this->client->send(new BackupUsers);
+ 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 38a01bb..66075d4 100644
--- a/src/HestiaCP/Module/Webs.php
+++ b/src/HestiaCP/Module/Webs.php
@@ -1,8 +1,9 @@
client->send(new WebDomain($this->user, $domain, $ip, $aliases, $proxyExtensions, $restart));
}
/**
- * This function for suspending the site's operation.
- *
+ * This function for suspending the site's operation.
+ *
* @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));
@@ -57,12 +57,12 @@ public function suspendDomain(string $domain, bool $restart = false): bool {
/**
* This function of unsuspending the domain.
- *
+ *
* @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));
@@ -70,11 +70,11 @@ 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\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));
@@ -82,26 +82,28 @@ 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 $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\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ * @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));
}
/**
* This function turns off letsencrypt SSL support for a domain.
- *
+ *
* @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));
@@ -109,28 +111,28 @@ 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
* @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));
}
/**
* This function changes ftp user password.
- *
+ *
* @param string $domain
* @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));
@@ -138,13 +140,13 @@ public function changeDomainFtpPassword(string $domain, string $ftpUser, string
/**
* This function changes ftp user path.
- *
+ *
* @param string $domain
* @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));
@@ -152,12 +154,12 @@ public function changeDomainFtpPath(string $domain, string $ftpUser, string $ftp
/**
* This function deletes additional ftp account.
- *
+ *
* @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));
@@ -165,10 +167,10 @@ public function deleteDomainFtp(string $domain, string $ftpUser): bool {
/**
* This function to obtain the list of all user web domains.
- *
- * @return ArrayHash[]
- * @throws \neto737\HestiaCP\ClientException
- * @throws \neto737\HestiaCP\ProcessException
+ *
+ * @return array>
+ * @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
new file mode 100644
index 0000000..464da43
--- /dev/null
+++ b/tests/clientSendTest.php
@@ -0,0 +1,359 @@
+}
+ */
+class clientSendTest extends \PHPUnit\Framework\TestCase {
+
+ /** @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));
+ $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',
+ 'arg4' => 'default',
+ 'returncode' => 'yes',
+ 'cmd' => 'v-add-user'
+ ], $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',
+ 'arg4' => 'default',
+ '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' => ['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->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 {
+ $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));
+ }
+
+ // 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)]);
+
+ $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_FORBIDDEN, 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/clientTest.php b/tests/clientTest.php
index fc37177..60bdf82 100644
--- a/tests/clientTest.php
+++ b/tests/clientTest.php
@@ -1,5 +1,7 @@
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 +25,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/commandAddTest.php b/tests/commandAddTest.php
index 9b3c561..8247a00 100644
--- a/tests/commandAddTest.php
+++ b/tests/commandAddTest.php
@@ -1,5 +1,7 @@
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());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$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());
@@ -37,7 +70,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 +87,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,10 +103,26 @@ public function testLEDomain(): void {
$this->assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$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());
@@ -86,7 +135,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 +152,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 +169,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 f054b01..f344a69 100644
--- a/tests/commandBackupTest.php
+++ b/tests/commandBackupTest.php
@@ -1,5 +1,7 @@
assertSame('v-backup-users', $command->getName());
- $this->assertSame(['arg1' => 'json'], $command->getRequestParams());
+ public function testBackupUsers(): void {
+ $command = new Users();
+ $this->assertSame('v-backup-users', $command->getName());
+ $this->assertSame([], $command->getRequestParams());
- $this->expectException(TypeError::class);
+ $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(TypeError::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
- }
+ }
}
diff --git a/tests/commandChangeTest.php b/tests/commandChangeTest.php
index 4a9fe02..8d84e7d 100644
--- a/tests/commandChangeTest.php
+++ b/tests/commandChangeTest.php
@@ -1,5 +1,7 @@
assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -32,7 +34,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 +50,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 +66,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..2c917f5 100644
--- a/tests/commandDeleteTest.php
+++ b/tests/commandDeleteTest.php
@@ -1,5 +1,7 @@
assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -36,7 +38,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 +53,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 +68,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 +83,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 +97,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 +111,7 @@ public function testUserBackup(): void {
$this->assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -120,9 +122,14 @@ 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\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -136,7 +143,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 +156,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 a8e1a1d..67355aa 100644
--- a/tests/commandListTest.php
+++ b/tests/commandListTest.php
@@ -1,5 +1,7 @@
assertFalse($command->needReturnCode());
- $this->expectException(TypeError::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -33,8 +35,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 +43,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,22 +51,23 @@ 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 {
$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 {
@@ -74,8 +75,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,16 +83,15 @@ 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 {
$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(TypeError::class);
+
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -101,8 +100,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 +108,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\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -119,7 +117,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/commandSuspendTest.php b/tests/commandSuspendTest.php
index 5ff7c66..66b0419 100644
--- a/tests/commandSuspendTest.php
+++ b/tests/commandSuspendTest.php
@@ -1,5 +1,7 @@
assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -26,12 +28,12 @@ public function testWebDomain(): void {
$this->assertSame([
'arg1' => 'admin',
'arg2' => 'domain.com',
- 'arg3' => 'no'
+ 'arg3' => 'no'
], $command->getRequestParams());
$this->assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -46,7 +48,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 +62,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 400f1f9..c2f5a55 100644
--- a/tests/commandTest.php
+++ b/tests/commandTest.php
@@ -1,8 +1,9 @@
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();
}
}
diff --git a/tests/commandUnsuspendTest.php b/tests/commandUnsuspendTest.php
index e4cbcf8..4c53321 100644
--- a/tests/commandUnsuspendTest.php
+++ b/tests/commandUnsuspendTest.php
@@ -1,5 +1,7 @@
assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -26,12 +28,12 @@ public function testWebDomain(): void {
$this->assertSame([
'arg1' => 'admin',
'arg2' => 'domain.com',
- 'arg3' => 'no'
+ 'arg3' => 'no'
], $command->getRequestParams());
$this->assertTrue($command->needReturnCode());
- $this->expectException(\neto737\HestiaCP\InvalidResponseException::class);
+ $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class);
$command->process();
}
@@ -46,7 +48,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 +62,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/credentialTest.php b/tests/credentialTest.php
index a59cb2e..300546b 100644
--- a/tests/credentialTest.php
+++ b/tests/credentialTest.php
@@ -1,12 +1,14 @@
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,11 +32,40 @@ 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());
$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(): void {
+ $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(): void {
+ $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(): 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 4c29162..01bed59 100644
--- a/tests/hostTest.php
+++ b/tests/hostTest.php
@@ -1,32 +1,100 @@
assertSame('https://hestia', $host->getHostname());
- $this->assertSame('', $host->getHostname());
+ $host->setHostname('http://host');
+ $this->assertSame('http://host', $host->getHostname());
+ }
- $host->setHostname('host');
- $this->assertSame('host', $host->getHostname());
+ /** @return array */
+ 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]'],
+ ];
}
- public function testUri() {
+ #[\PHPUnit\Framework\Attributes\DataProvider('normalizedHostnames')]
+ public function testNormalizesHostname(string $hostname, string $expected): void {
+ $host = new Host($hostname, new Credentials('', ''));
+
+ $this->assertSame($expected, $host->getHostname());
+ }
+
+ /** @return array */
+ 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): void {
+ $this->expectException(InvalidArgumentException::class);
+
+ new Host($hostname, new Credentials('', ''));
+ }
+
+ 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(): void {
+ $host = new Host('https://hestia', new Credentials('', ''));
+
+ try {
+ $host->setHostname('hestia');
+ $this->fail('Expected InvalidArgumentException');
+ } catch (InvalidArgumentException $e) {
+ $this->assertSame('https://hestia', $host->getHostname());
+ }
+ }
+
+ public function testUri(): void {
$credentials = new Credentials('', '');
$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() {
+ public function testPort(): void {
$credentials = new Credentials('', '');
- $host = new Host('', $credentials);
+ $host = new Host('https://hestia', $credentials);
$this->assertSame(8083, $host->getPort());
@@ -34,9 +102,9 @@ public function testPort() {
$this->assertSame(2023, $host->getPort());
}
- public function testCredentials() {
+ public function testCredentials(): void {
$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..a9183a1 100644
--- a/tests/moduleTest.php
+++ b/tests/moduleTest.php
@@ -1,5 +1,7 @@
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'));
+ }
}