diff --git a/CHANGELOG.md b/CHANGELOG.md index d400a24..284d1ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [2.1.0] - 2026-10-18 + ### Added +- `RawCommand` and `RawListCommand` run any HestiaCP script by name, for scripts with no command class: `new RawCommand('v-add-firewall-rule', ['DROP', '203.0.113.7', '25'])` returns `true`, and `new RawListCommand('v-list-firewall', ['json'])` returns the decoded JSON. A name that isn't a HestiaCP script name throws an `InvalidArgumentException`. +- Databases: `changePassword()`, `suspend()` and `unsuspend()`, with the command classes `ChangeDatabasePassword`, `SuspendDatabase` and `UnsuspendDatabase` (`v-change-database-password`, `v-suspend-database`, `v-unsuspend-database`). +- Users: `changeContact()`, `changePackage()` and `stats()`, with the command classes `ChangeUserContact`, `ChangeUserPackage` and `ListUserStats` (`v-change-user-contact`, `v-change-user-package`, `v-list-user-stats`). +- Package module, `Client::getModulePackage()`: `list()` and `detail()`, with the command classes `Package\ListUserPackages` and `Package\ListUserPackage` (`v-list-user-packages`, `v-list-user-package`). +- Cron module, `Client::getModuleCron($user)`: `listJobs()`, `listJob()`, `addJob()`, `changeJob()`, `suspendJob()`, `unsuspendJob()` and `deleteJob()`, with the command classes in `Command\Cron` (`v-list-cron-jobs`, `v-list-cron-job`, `v-add-cron-job`, `v-change-cron-job`, `v-suspend-cron-job`, `v-unsuspend-cron-job`, `v-delete-cron-job`). The optional `$restart` argument is only sent when set, so each script keeps HestiaCP's own default. +- Web: `listDomain()`, aliases (`addDomainAlias()`, `deleteDomainAlias()`), SSL (`listDomainSsl()`, `addDomainSsl()`, `changeDomainSslCert()`, `changeDomainSslHome()`, `deleteDomainSsl()`, `addDomainSslForce()`, `deleteDomainSslForce()`, `addDomainSslHsts()`, `deleteDomainSslHsts()`), templates (`listTemplates()`, `listBackendTemplates()`, `listProxyTemplates()`, `changeDomainTemplate()`, `changeDomainBackendTemplate()`, `changeDomainProxyTemplate()`) and the proxy (`addDomainProxy()`, `deleteDomainProxy()`), with the command classes in `Command\Web` (`v-list-web-domain`, `v-add-web-domain-alias`, `v-delete-web-domain-alias`, `v-list-web-domain-ssl`, `v-add-web-domain-ssl`, `v-change-web-domain-sslcert`, `v-change-web-domain-sslhome`, `v-delete-web-domain-ssl`, `v-add-web-domain-ssl-force`, `v-delete-web-domain-ssl-force`, `v-add-web-domain-ssl-hsts`, `v-delete-web-domain-ssl-hsts`, `v-list-web-templates`, `v-list-web-templates-backend`, `v-list-web-templates-proxy`, `v-change-web-domain-tpl`, `v-change-web-domain-backend-tpl`, `v-change-web-domain-proxy-tpl`, `v-add-web-domain-proxy`, `v-delete-web-domain-proxy`). Like the cron module, the optional `$restart` argument is only sent when set. The proxy methods throw an `InvalidArgumentException` when `$restart` is set without `$extensions`, because HestiaCP would read the empty extensions as none. +- Mail: `listDomain()`, `listAccount()`, aliases (`addAccountAlias()`, `deleteAccountAlias()`), forwarding (`addAccountForward()`, `deleteAccountForward()`, `addAccountForwardOnly()`, `deleteAccountForwardOnly()`) and autoreplies (`listAccountAutoreply()`, `addAccountAutoreply()`, `deleteAccountAutoreply()`), with the command classes in `Command\Mail` (`v-list-mail-domain`, `v-list-mail-account`, `v-add-mail-account-alias`, `v-delete-mail-account-alias`, `v-add-mail-account-forward`, `v-delete-mail-account-forward`, `v-add-mail-account-fwd-only`, `v-delete-mail-account-fwd-only`, `v-list-mail-account-autoreply`, `v-add-mail-account-autoreply`, `v-delete-mail-account-autoreply`). +- DNS module, `Client::getModuleDns($user)`: domains (`listDomains()`, `listDomain()`, `addDomain()`, `deleteDomain()`, `suspendDomain()`, `unsuspendDomain()`, `changeDomainIp()`, `changeDomainTtl()`, `changeDomainExpiration()`, `changeDomainSoa()`, `changeDomainTemplate()`), DNSSEC (`changeDomainDnssec()`, `listDnssecPublicKey()`), records (`listRecords()`, `addRecord()`, `changeRecord()`, `changeRecordId()`, `suspendRecord()`, `unsuspendRecord()`, `deleteRecord()`) and templates (`listTemplates()`, `listTemplate()`), with the command classes in `Command\Dns` (`v-list-dns-domains`, `v-list-dns-domain`, `v-add-dns-domain`, `v-delete-dns-domain`, `v-suspend-dns-domain`, `v-unsuspend-dns-domain`, `v-change-dns-domain-ip`, `v-change-dns-domain-ttl`, `v-change-dns-domain-exp`, `v-change-dns-domain-soa`, `v-change-dns-domain-tpl`, `v-change-dns-domain-dnssec`, `v-list-dnssec-public-key`, `v-list-dns-records`, `v-add-dns-record`, `v-change-dns-record`, `v-change-dns-record-id`, `v-suspend-dns-record`, `v-unsuspend-dns-record`, `v-delete-dns-record`, `v-list-dns-templates`, `v-list-dns-template`). Like the cron module, the optional `$restart` argument is only sent when set. `addDomain()` takes the nameservers as an array and throws an `InvalidArgumentException` for exactly one, which HestiaCP would ignore, or more than eight. - `Command::positional()` builds the `arg1`, `arg2`, … request parameters from a list of arguments. A `null` argument is left out when nothing after it is set and sent as an empty string otherwise. Every built-in command uses it, and it has no limit on the number of arguments. - `ICommand` is generic for static analysis (`ICommand`, `process()` returns `T`), and `Client::send()` returns the command's result type. `ProcessCommand` is an `ICommand` and `ListCommand` an `ICommand>`. Custom commands that extend `Command` directly can declare theirs with `@extends Command<...>`. Docblocks only, nothing changes at runtime. @@ -101,7 +111,8 @@ This release brings the library up to date with HestiaCP 1.10 and contains break First release, with the user, web and mail modules. -[Unreleased]: https://github.com/neto737/HestiaCP-API/compare/v2.0.0...HEAD +[Unreleased]: https://github.com/neto737/HestiaCP-API/compare/v2.1.0...HEAD +[2.1.0]: https://github.com/neto737/HestiaCP-API/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/neto737/HestiaCP-API/compare/v1.2.0...v2.0.0 [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 diff --git a/README.md b/README.md index 1c4ba75..7194d9d 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ ## Features - **Every HestiaCP login method:** access keys (HestiaCP 1.6+), legacy API keys, and admin username and password. -- **Ready-made modules** for users, web domains, mail, databases and backups. +- **Ready-made modules** for users, web domains, mail, DNS, databases, backups, cron jobs and packages. - **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. +- **Lower-level access:** send any of the bundled command classes directly, run any `v-*` script by name with `RawCommand` and `RawListCommand`, or write your own. - Tested on PHP 8.2 to 8.5 and checked against HestiaCP 1.10. ## Requirements @@ -73,13 +73,16 @@ $mail->addAccount('example.com', 'info', 'S3cure-Pa$$word'); | 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. | +| [Users](docs/users.md) | `getModuleUser()` | List, show, add and delete users. Change passwords, contact emails and packages, suspend and unsuspend, list usage stats, back up all users. | +| [Web domains](docs/web.md) | `getModuleWeb($user)` | Add, delete, suspend and unsuspend domains. Aliases, SSL certificates, Let's Encrypt, templates, proxy and FTP accounts. | +| [Mail](docs/mail.md) | `getModuleMail($user)` | Mail domains and accounts, passwords, suspending, aliases, forwarding, autoreplies, DKIM keys and DNS records. | +| [Databases](docs/databases.md) | `getModuleDatabase($user)` | Create, list and delete databases. Change passwords, suspend and unsuspend. | | [Backups](docs/backups.md) | `getModuleBackup($user)` | Create, list and delete backups. List and delete backup exclusions. | +| [Packages](docs/packages.md) | `getModulePackage()` | List and show hosting packages. | +| [Cron jobs](docs/cron.md) | `getModuleCron($user)` | List, add, change, suspend, unsuspend and delete cron jobs. | +| [DNS](docs/dns.md) | `getModuleDns($user)` | DNS domains and records: add, change, suspend, unsuspend and delete. TTL, SOA, templates and DNSSEC. | -Need a command that isn't wrapped in a module? See [Sending commands directly](docs/commands.md). +Need a command that isn't wrapped in a module? See [Sending commands directly](docs/commands.md), or [run any script by name](docs/commands.md#running-any-script) when no command class exists for it. ## Error handling diff --git a/composer.json b/composer.json index 643dced..fb1f9fa 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "neto737/hestiacp-api", - "description": "PHP client for the HestiaCP API: users, web domains, mail, databases and backups", + "description": "PHP client for the HestiaCP API: users, web domains, mail, DNS, databases, backups, cron jobs and packages", "type": "library", "keywords": [ "hestiacp", diff --git a/docs/commands.md b/docs/commands.md index 5c856c7..2bae947 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -6,7 +6,7 @@ The modules are a convenience layer. Each method creates a command object and pa Each class is named after the HestiaCP script it runs: drop the `v-` prefix and PascalCase the rest. `v-list-users` is `ListUsers`, `v-change-user-password` is `ChangeUserPassword`, and `v-add-letsencrypt-domain` is `AddLetsEncryptDomain`. -The classes are grouped by the module that sends them: `Command\User`, `Command\Web`, `Command\Mail`, `Command\Database` and `Command\Backup`. For example, `v-add-letsencrypt-domain` is sent by the web module, so its class is `Command\Web\AddLetsEncryptDomain`. +The classes are grouped by the module that sends them: `Command\User`, `Command\Web`, `Command\Mail`, `Command\Database`, `Command\Backup`, `Command\Package`, `Command\Cron` and `Command\Dns`. For example, `v-add-letsencrypt-domain` is sent by the web module, so its class is `Command\Web\AddLetsEncryptDomain`. ```php use neto737\HestiaCP\Command\User\ChangeUserPassword; @@ -29,6 +29,24 @@ echo $command->getResponseText(); // the JSON returned by HestiaCP echo $command->getStatusCode(); // the HTTP status code ``` +## Running any script + +For a script that has no command class, use `RawCommand` or `RawListCommand` with the script name and its arguments, in order: + +```php +use neto737\HestiaCP\Command\RawCommand; +use neto737\HestiaCP\Command\RawListCommand; + +$client->send(new RawCommand('v-add-firewall-rule', ['DROP', '203.0.113.7', '25'])); // true + +$rules = $client->send(new RawListCommand('v-list-firewall', ['json'])); // array +``` + +- `RawCommand` is for scripts that change something. Like `ProcessCommand`, it returns `true` on exit code 0 and throws an exception otherwise. +- `RawListCommand` is for scripts that print JSON. Like `ListCommand`, it returns the decoded array. Most list scripts print JSON only when you pass `json` as their format argument, so include it in the arguments. + +The arguments go through `positional()` (see below), so `null` works the same way for optional arguments. A name that isn't a HestiaCP script name (`v-` followed by lowercase words joined by dashes) throws an `InvalidArgumentException` before anything is sent. + ## Writing your own command Extend one of the two base classes: @@ -41,55 +59,49 @@ Implement `getName()` (the HestiaCP script name) and `getRequestParams()` (the s ```php use neto737\HestiaCP\Command\ProcessCommand; -class AddCronJob extends ProcessCommand { +class AddFirewallRule 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 + private string $action, + private string $ip, + private string $port, + private ?string $protocol = null, + private ?string $comment = null ) {} public function getName(): string { - return 'v-add-cron-job'; + return 'v-add-firewall-rule'; } public function getRequestParams(): array { return $this->positional([ - $this->user, - $this->min, - $this->hour, - $this->day, - $this->month, - $this->weekday, - $this->command, + $this->action, + $this->ip, + $this->port, + $this->protocol, + $this->comment, ]); } } -$client->send(new AddCronJob('admin', '0', '3', '*', '*', '*', '/usr/bin/php /home/admin/cron.php')); +$client->send(new AddFirewallRule('DROP', '203.0.113.7', '25')); ``` ```php use neto737\HestiaCP\Command\ListCommand; -class ListCronJobs extends ListCommand { - - public function __construct(private string $user) {} +class ListFirewall extends ListCommand { public function getName(): string { - return 'v-list-cron-jobs'; + return 'v-list-firewall'; } public function getRequestParams(): array { - return $this->positional([$this->user, 'json']); + return $this->positional(['json']); } } -$jobs = $client->send(new ListCronJobs('admin')); +$rules = $client->send(new ListFirewall()); ``` Most HestiaCP list scripts print JSON only when you pass `json` as their format argument. diff --git a/docs/cron.md b/docs/cron.md new file mode 100644 index 0000000..820d39e --- /dev/null +++ b/docs/cron.md @@ -0,0 +1,58 @@ +# Cron jobs + +```php +$cron = $client->getModuleCron('admin'); +``` + +The cron module works on the cron jobs of one user, given when you get the module. Jobs are identified by their numeric ID. + +## Listing + +```php +$cron->listJobs(); // [7 => ['MIN' => '0', 'HOUR' => '3', 'CMD' => '...', 'SUSPENDED' => 'no', ...], ...] + +$cron->listJob(7); // the fields of one job +``` + +## Adding + +The schedule takes the five crontab fields as strings: minute, hour, day of month, month, day of week. + +```php +$cron->addJob('0', '3', '*', '*', '*', '/usr/bin/php /home/admin/cron.php'); // every day at 03:00 + +$cron->addJob('*/5', '*', '*', '*', '1-5', 'uptime', 20); // with job ID 20 +``` + +Without a job ID, HestiaCP gives the job the next free ID. `addJob()` returns `true`, not the ID, so call `listJobs()` to find it, or pass your own. + +## Changing + +```php +$cron->changeJob(7, '30', '4', '*', '*', '0', '/usr/bin/php /home/admin/cron.php'); +``` + +`changeJob()` replaces the whole schedule and command, so pass every field. + +## Suspending and deleting + +```php +$cron->suspendJob(7); +$cron->unsuspendJob(7); + +$cron->deleteJob(7); +``` + +## Restarting the cron service + +`addJob()`, `suspendJob()`, `unsuspendJob()` and `deleteJob()` take an optional last argument, `$restart`: + +- `null` (default) leaves it to HestiaCP. `unsuspendJob()` restarts the cron service by default; the others restart it only when the server is set up for scheduled restarts. +- `true` restarts the cron service right away. +- `false` doesn't restart it. + +```php +$cron->deleteJob(7, false); +``` + +All methods except `listJobs()` and `listJob()` return `true` on success and throw an exception on failure (see [Error handling](errors.md)). diff --git a/docs/databases.md b/docs/databases.md index 206bb68..3231aac 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -24,6 +24,19 @@ $db->listDatabases(); // ['admin_database' => ['DATABASE' => '...', 'DBUSER' => $db->listDatabase('admin_database'); // the fields of one database ``` +## Changing the password + +```php +$db->changePassword('admin_database', 'newPa$$word'); // the password of the database user +``` + +## Suspending + +```php +$db->suspend('admin_database'); +$db->unsuspend('admin_database'); +``` + ## Deleting ```php diff --git a/docs/dns.md b/docs/dns.md new file mode 100644 index 0000000..6762909 --- /dev/null +++ b/docs/dns.md @@ -0,0 +1,124 @@ +# DNS + +```php +$dns = $client->getModuleDns('admin'); +``` + +The DNS module works on the DNS domains (zones) of one user, given when you get the module. + +## Domains + +```php +$dns->listDomains(); // ['example.com' => ['IP' => '...', 'TPL' => 'default', 'TTL' => '14400', 'RECORDS' => '...', ...], ...] +$dns->listDomain('example.com'); // the fields of one domain + +$dns->addDomain('example.com', '192.0.2.10'); + +$dns->suspendDomain('example.com'); +$dns->unsuspendDomain('example.com'); + +$dns->deleteDomain('example.com'); +``` + +`addDomain()` creates the zone with the records of the user's DNS template, pointed at the IP address. It also takes these optional arguments: + +```php +$dns->addDomain( + 'example.com', + '192.0.2.10', + ['ns1.example.org', 'ns2.example.org'], // nameservers (default: the user's) + true, // restart the DNS server + true // turn on DNSSEC +); +``` + +Pass two to eight nameservers, or none. HestiaCP ignores a single nameserver and uses the user's instead, so `addDomain()` throws an `InvalidArgumentException` for one, and for more than eight. + +## Changing a domain + +```php +$dns->changeDomainIp('example.com', '192.0.2.20'); // in the domain and in every record that holds the old IP +$dns->changeDomainTtl('example.com', 3600); // in seconds +$dns->changeDomainExpiration('example.com', '2027-10-18'); // YYYY-MM-DD +$dns->changeDomainSoa('example.com', 'ns1.example.org'); // primary nameserver in the SOA record +$dns->changeDomainTemplate('example.com', 'gmail'); +``` + +`changeDomainTemplate()` replaces the domain's records with the records of the new template, so records you added yourself are lost. + +## DNSSEC + +```php +$dns->changeDomainDnssec('example.com', true); // turn on +$dns->changeDomainDnssec('example.com', false); // turn off and delete the keys + +$dns->listDnssecPublicKey('example.com'); // ['RECORD' => '...', 'KEYTAG' => '...', 'ALGORITHM' => '...', 'KEY' => '...', 'DS' => '...', ...] +``` + +HestiaCP supports DNSSEC only when `DNS_CLUSTER_SYSTEM` is `hestia-zone` and BIND is newer than 9.16.18. Otherwise `changeDomainDnssec()` throws a `ProcessException` with the code `ResponseCode::E_DISABLED`. + +## Records + +Records are identified by their numeric ID. + +```php +$dns->listRecords('example.com'); // [1 => ['RECORD' => '@', 'TYPE' => 'A', 'VALUE' => '192.0.2.10', 'TTL' => '14400', ...], ...] + +$dns->addRecord('example.com', 'www', 'A', '192.0.2.10'); +$dns->addRecord('example.com', '@', 'MX', 'mail.example.com', 20); // with priority 20 + +$dns->suspendRecord('example.com', 7); +$dns->unsuspendRecord('example.com', 7); + +$dns->deleteRecord('example.com', 7); +``` + +The record name is relative to the domain, with `@` for the domain itself. HestiaCP adds the trailing dot to the value of NS, CNAME, MX, PTR and SRV records. + +`addRecord()` also takes these optional arguments, after the priority: + +```php +$dns->addRecord( + 'example.com', + '_dmarc', + 'TXT', + 'v=DMARC1; p=none', + null, // priority, only used by MX and SRV records (default: 10) + 30, // record ID (default: the next free ID) + true, // restart the DNS server + 300 // TTL in seconds (default: the domain's TTL) +); +``` + +`addRecord()` returns `true`, not the ID, so call `listRecords()` to find it, or pass your own. + +### Changing a record + +```php +$dns->changeRecord('example.com', 7, 'www', 'A', '192.0.2.20'); + +$dns->changeRecordId('example.com', 7, 30); // record 7 becomes record 30 +``` + +`changeRecord()` replaces the record, so pass its name, type and value even if they don't change. The optional arguments are the priority (default: the current one), `$restart` and the TTL. Without a TTL, the record goes back to the domain's TTL. If nothing changes, HestiaCP fails with `ResponseCode::E_EXISTS`. + +## Templates + +```php +$dns->listTemplates(); // ['default', 'gmail', 'office365', ...] +$dns->listTemplate('gmail'); // the records of a template, keyed by ID +``` + +## Restarting the DNS server + +`addDomain()`, `deleteDomain()`, `changeDomainIp()`, `changeDomainTtl()`, `changeDomainSoa()`, `changeDomainTemplate()`, `addRecord()`, `changeRecord()`, `changeRecordId()` and `deleteRecord()` take an optional `$restart` argument: + +- `null` (default) leaves it to HestiaCP, which restarts the DNS server right away, or later when the server is set up for scheduled restarts. +- `true` restarts the DNS server right away. +- `false` doesn't restart it. + +```php +$dns->deleteRecord('example.com', 7, false); +``` + +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 index 56a67d0..9b14fe3 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -33,7 +33,7 @@ All classes are in the `neto737\HestiaCP\Exceptions` namespace. | `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. | +| `InvalidArgumentException` | `Exception` | An argument is invalid before anything is sent, for example a malformed hostname or a `RawCommand` name that isn't a HestiaCP script name. | ## Exit codes diff --git a/docs/mail.md b/docs/mail.md index 105fe10..d0b3378 100644 --- a/docs/mail.md +++ b/docs/mail.md @@ -10,6 +10,7 @@ The mail module works on the mail domains of one user, given when you get the mo ```php $mail->listDomains(); // ['example.com' => [...], ...] +$mail->listDomain('example.com'); // the fields of one domain $mail->addDomain('example.com'); @@ -36,6 +37,7 @@ $mail->listDomainDkimDns('example.com'); // the DNS record to publish ```php $mail->listAccounts('example.com'); // ['info' => [...], ...] +$mail->listAccount('example.com', 'info'); // ['ALIAS' => '...', 'FWD' => '...', 'FWD_ONLY' => '...', 'AUTOREPLY' => '...', ...] $mail->addAccount('example.com', 'info', 'pa$$word'); // creates info@example.com @@ -49,4 +51,47 @@ $mail->deleteAccount('example.com', 'info'); Accounts are given by the part before the `@`. +## Aliases + +An alias is another address on the same domain that delivers to the account. Like accounts, it's given by the part before the `@`. + +```php +$mail->addAccountAlias('example.com', 'info', 'sales'); // sales@example.com delivers to info@example.com + +$mail->deleteAccountAlias('example.com', 'info', 'sales'); +``` + +`listAccount()` shows an account's aliases, comma-separated, in `ALIAS`. + +## Forwarding + +Forwards take the full address, one at a time: + +```php +$mail->addAccountForward('example.com', 'info', 'bob@example.org'); + +$mail->deleteAccountForward('example.com', 'info', 'bob@example.org'); +``` + +By default, forwarded mail is also kept in the account's mailbox. To only forward it: + +```php +$mail->addAccountForwardOnly('example.com', 'info'); // forward without keeping a copy +$mail->deleteAccountForwardOnly('example.com', 'info'); // keep a copy again +``` + +`listAccount()` shows the forwards, comma-separated, in `FWD`, and whether the account is forward only in `FWD_ONLY`. + +## Autoreply + +```php +$mail->addAccountAutoreply('example.com', 'info', "I'm away until Monday.\nFor urgent matters, write to bob@example.org."); + +$mail->listAccountAutoreply('example.com', 'info'); // ['MSG' => "I'm away until Monday.\n..."] + +$mail->deleteAccountAutoreply('example.com', 'info'); +``` + +Calling `addAccountAutoreply()` again replaces the message. + 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/packages.md b/docs/packages.md new file mode 100644 index 0000000..ea6a183 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,19 @@ +# Packages + +```php +$packages = $client->getModulePackage(); +``` + +The package module works on the server's hosting packages, so it takes no username. + +## Listing + +```php +$packages->list(); // ['default' => ['WEB_DOMAINS' => '...', 'DISK_QUOTA' => '...', ...], ...] + +$packages->detail('default'); // the fields of one package +``` + +`list()` returns every package, keyed by name. `detail()` returns the fields of one package. + +To move a user to another package, use `changePackage()` in the [user module](users.md#changing-the-password-contact-email-or-package). diff --git a/docs/users.md b/docs/users.md index 23cf32c..2bbef0d 100644 --- a/docs/users.md +++ b/docs/users.md @@ -17,6 +17,12 @@ echo $admin['CONTACT']; `list()` returns every user, keyed by username. `detail()` returns the fields of one user. +```php +$users->stats('admin'); // ['2026-10-01' => ['U_DISK' => '...', 'U_BANDWIDTH' => '...', ...], ...] +``` + +`stats()` returns the user's monthly usage statistics, keyed by date. + ## Adding ```php @@ -28,12 +34,19 @@ $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 +## Changing the password, contact email or package ```php $users->changePassword('other_user', 'otherPa$$word'); + +$users->changeContact('other_user', 'new@email.com'); + +$users->changePackage('other_user', 'gold'); +$users->changePackage('other_user', 'gold', true); // even if the user's current usage exceeds the package limits ``` +To see the available packages, use the [package module](packages.md). + ## Suspending and deleting ```php @@ -53,4 +66,4 @@ $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)). +All methods except `list()`, `detail()` and `stats()` 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 index dd85c5d..d11c227 100644 --- a/docs/web.md +++ b/docs/web.md @@ -10,6 +10,7 @@ The web module works on the domains of one user, given when you get the module. ```php $web->listDomains(); // ['example.com' => ['IP' => '...', 'ALIAS' => '...', ...], ...] +$web->listDomain('example.com'); // the fields of one domain $web->addDomain('example.com'); @@ -33,6 +34,48 @@ $web->addDomain( `suspendDomain()` and `unsuspendDomain()` take an optional `$restart` argument (default `false`). +## Aliases + +```php +$web->addDomainAlias('example.com', 'www.example.com,shop.example.com'); // comma-separated + +$web->deleteDomainAlias('example.com', 'shop.example.com'); // one at a time +``` + +## SSL certificates + +```php +$web->listDomainSsl('example.com'); // ['CRT' => '...', 'KEY' => '...', 'CA' => '...', 'SUBJECT' => '...', 'NOT_AFTER' => '...', ...] +``` + +To use your own certificate, put `example.com.crt`, `example.com.key` and, if you have one, `example.com.ca` in a directory **on the HestiaCP server**, and pass that directory. HestiaCP copies the files, so the directory can be deleted afterwards. The API can't upload the files for you. + +```php +use neto737\HestiaCP\Command\Web\AddWebDomainSsl; + +$web->addDomainSsl('example.com', '/home/admin/tmp/ssl'); + +// serve HTTPS from public_shtml instead of public_html +$web->addDomainSsl('example.com', '/home/admin/tmp/ssl', AddWebDomainSsl::SSL_HOME_SINGLE); + +$web->changeDomainSslCert('example.com', '/home/admin/tmp/ssl-renewed'); // replace the certificate +$web->changeDomainSslHome('example.com', AddWebDomainSsl::SSL_HOME_SAME); + +$web->deleteDomainSsl('example.com'); +``` + +The SSL home is `same` (HTTPS serves `public_html`, the default) or `single` (HTTPS serves `public_shtml`). Any other value throws an `InvalidArgumentException`. + +Redirecting HTTP to HTTPS and HSTS need SSL to be on: + +```php +$web->addDomainSslForce('example.com'); +$web->deleteDomainSslForce('example.com'); + +$web->addDomainSslHsts('example.com'); +$web->deleteDomainSslHsts('example.com'); +``` + ## Let's Encrypt ```php @@ -51,6 +94,32 @@ Getting a certificate is slow, so this command uses a timeout of at least 90 sec 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. +## Templates + +The template lists are server-wide, so they're the same whichever user the module is for. + +```php +$web->listTemplates(); // ['default', 'hosting', ...] +$web->listBackendTemplates(); // ['default', 'PHP-8_3', ...] +$web->listProxyTemplates(); // ['caching', 'default', ...] + +$web->changeDomainTemplate('example.com', 'hosting'); +$web->changeDomainBackendTemplate('example.com', 'PHP-8_3'); +$web->changeDomainProxyTemplate('example.com', 'caching'); +$web->changeDomainProxyTemplate('example.com', 'caching', 'jpg,png,css,js'); // and the file extensions served by the proxy +``` + +Without extensions, `changeDomainProxyTemplate()` resets them to HestiaCP's default list. + +## Proxy + +```php +$web->addDomainProxy('example.com'); // the user's default proxy template +$web->addDomainProxy('example.com', 'caching', 'jpg,png,css,js'); + +$web->deleteDomainProxy('example.com'); +``` + ## FTP accounts ```php @@ -65,4 +134,18 @@ $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)). +## Restarting the web server + +The alias, SSL, template and proxy methods take an optional last argument, `$restart`: + +- `null` (default) leaves it to HestiaCP, which restarts the affected services right away, or queues the restart when the server is set up for scheduled restarts. +- `true` restarts them right away. +- `false` doesn't restart them. + +```php +$web->addDomainAlias('example.com', 'www.example.com', false); +``` + +For `addDomainProxy()` and `changeDomainProxyTemplate()`, pass the extensions when you pass `$restart`. HestiaCP reads an empty extensions argument as no extensions, not as its default list, so leaving them `null` with `$restart` set throws an `InvalidArgumentException`. Pass `''` if you do want no extensions. + +All methods except the `list*()` methods return `true` on success and throw an exception on failure (see [Error handling](errors.md)). diff --git a/src/HestiaCP/Client.php b/src/HestiaCP/Client.php index 2a49421..4eb813b 100644 --- a/src/HestiaCP/Client.php +++ b/src/HestiaCP/Client.php @@ -12,9 +12,12 @@ use neto737\HestiaCP\Exceptions\InvalidArgumentException; use neto737\HestiaCP\Exceptions\ProcessException; use neto737\HestiaCP\Module\Backups; +use neto737\HestiaCP\Module\Crons; use neto737\HestiaCP\Module\Databases; +use neto737\HestiaCP\Module\Dns; use neto737\HestiaCP\Module\Mails; use neto737\HestiaCP\Module\Module; +use neto737\HestiaCP\Module\Packages; use neto737\HestiaCP\Module\Users; use neto737\HestiaCP\Module\Webs; @@ -199,4 +202,16 @@ public function getModuleBackup(string $user): Backups { public function getModuleDatabase(string $user): Databases { return $this->loadModule(Databases::class, $user); } + + public function getModulePackage(): Packages { + return $this->loadModule(Packages::class); + } + + public function getModuleCron(string $user): Crons { + return $this->loadModule(Crons::class, $user); + } + + public function getModuleDns(string $user): Dns { + return $this->loadModule(Dns::class, $user); + } } diff --git a/src/HestiaCP/Command/Cron/AddCronJob.php b/src/HestiaCP/Command/Cron/AddCronJob.php new file mode 100644 index 0000000..35a0cda --- /dev/null +++ b/src/HestiaCP/Command/Cron/AddCronJob.php @@ -0,0 +1,44 @@ +positional([ + $this->user, + $this->min, + $this->hour, + $this->day, + $this->month, + $this->wday, + $this->command, + $this->job === null ? null : (string) $this->job, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Cron/ChangeCronJob.php b/src/HestiaCP/Command/Cron/ChangeCronJob.php new file mode 100644 index 0000000..3238053 --- /dev/null +++ b/src/HestiaCP/Command/Cron/ChangeCronJob.php @@ -0,0 +1,38 @@ +positional([ + $this->user, + (string) $this->job, + $this->min, + $this->hour, + $this->day, + $this->month, + $this->wday, + $this->command + ]); + } +} diff --git a/src/HestiaCP/Command/Cron/DeleteCronJob.php b/src/HestiaCP/Command/Cron/DeleteCronJob.php new file mode 100644 index 0000000..b17d102 --- /dev/null +++ b/src/HestiaCP/Command/Cron/DeleteCronJob.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + (string) $this->job, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Cron/ListCronJob.php b/src/HestiaCP/Command/Cron/ListCronJob.php new file mode 100644 index 0000000..f5fd42e --- /dev/null +++ b/src/HestiaCP/Command/Cron/ListCronJob.php @@ -0,0 +1,27 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, (string) $this->job, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Cron/ListCronJobs.php b/src/HestiaCP/Command/Cron/ListCronJobs.php new file mode 100644 index 0000000..9fe1544 --- /dev/null +++ b/src/HestiaCP/Command/Cron/ListCronJobs.php @@ -0,0 +1,22 @@ +positional([$this->user, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Cron/SuspendCronJob.php b/src/HestiaCP/Command/Cron/SuspendCronJob.php new file mode 100644 index 0000000..59d60f3 --- /dev/null +++ b/src/HestiaCP/Command/Cron/SuspendCronJob.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + (string) $this->job, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Cron/UnsuspendCronJob.php b/src/HestiaCP/Command/Cron/UnsuspendCronJob.php new file mode 100644 index 0000000..d02078a --- /dev/null +++ b/src/HestiaCP/Command/Cron/UnsuspendCronJob.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + (string) $this->job, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Database/ChangeDatabasePassword.php b/src/HestiaCP/Command/Database/ChangeDatabasePassword.php new file mode 100644 index 0000000..1bcc33c --- /dev/null +++ b/src/HestiaCP/Command/Database/ChangeDatabasePassword.php @@ -0,0 +1,28 @@ +positional([ + $this->user, + $this->database, + $this->dbpass + ]); + } +} diff --git a/src/HestiaCP/Command/Database/SuspendDatabase.php b/src/HestiaCP/Command/Database/SuspendDatabase.php new file mode 100644 index 0000000..782efc3 --- /dev/null +++ b/src/HestiaCP/Command/Database/SuspendDatabase.php @@ -0,0 +1,23 @@ +positional([$this->user, $this->database]); + } +} diff --git a/src/HestiaCP/Command/Database/UnsuspendDatabase.php b/src/HestiaCP/Command/Database/UnsuspendDatabase.php new file mode 100644 index 0000000..8fd922a --- /dev/null +++ b/src/HestiaCP/Command/Database/UnsuspendDatabase.php @@ -0,0 +1,23 @@ +positional([$this->user, $this->database]); + } +} diff --git a/src/HestiaCP/Command/Dns/AddDnsDomain.php b/src/HestiaCP/Command/Dns/AddDnsDomain.php new file mode 100644 index 0000000..e559bf4 --- /dev/null +++ b/src/HestiaCP/Command/Dns/AddDnsDomain.php @@ -0,0 +1,49 @@ + $nameservers Two to eight nameservers, or none for the user's + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @param bool|null $dnssec Turn on DNSSEC, or null for HestiaCP's default (off) + * @throws InvalidArgumentException + */ + public function __construct( + private readonly string $user, + private readonly string $domain, + private readonly string $ip, + private readonly array $nameservers = [], + private readonly ?bool $restart = null, + private readonly ?bool $dnssec = null + ) { + // HestiaCP replaces fewer than two nameservers with the user's, so a single one would be dropped + if (count($nameservers) === 1) { + throw new InvalidArgumentException('Pass at least two nameservers, or none for the user\'s: HestiaCP ignores a single nameserver'); + } + if (count($nameservers) > 8) { + throw new InvalidArgumentException('HestiaCP takes at most 8 nameservers'); + } + } + + public function getName(): string { + return 'v-add-dns-domain'; + } + + public function getRequestParams(): array { + return $this->positional([ + $this->user, + $this->domain, + $this->ip, + ...array_pad($this->nameservers, 8, null), + $this->restart === null ? null : $this->convertBool($this->restart), + $this->dnssec === null ? null : $this->convertBool($this->dnssec) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/AddDnsRecord.php b/src/HestiaCP/Command/Dns/AddDnsRecord.php new file mode 100644 index 0000000..2341c59 --- /dev/null +++ b/src/HestiaCP/Command/Dns/AddDnsRecord.php @@ -0,0 +1,48 @@ +positional([ + $this->user, + $this->domain, + $this->record, + $this->type, + $this->value, + $this->priority === null ? null : (string) $this->priority, + $this->id === null ? null : (string) $this->id, + $this->restart === null ? null : $this->convertBool($this->restart), + $this->ttl === null ? null : (string) $this->ttl + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsDomainDnssec.php b/src/HestiaCP/Command/Dns/ChangeDnsDomainDnssec.php new file mode 100644 index 0000000..875a5d4 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsDomainDnssec.php @@ -0,0 +1,24 @@ +positional([$this->user, $this->domain, $this->convertBool($this->status)]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsDomainExp.php b/src/HestiaCP/Command/Dns/ChangeDnsDomainExp.php new file mode 100644 index 0000000..7113796 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsDomainExp.php @@ -0,0 +1,27 @@ +positional([$this->user, $this->domain, $this->exp]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsDomainIp.php b/src/HestiaCP/Command/Dns/ChangeDnsDomainIp.php new file mode 100644 index 0000000..90dbb7b --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsDomainIp.php @@ -0,0 +1,33 @@ +positional([ + $this->user, + $this->domain, + $this->ip, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsDomainSoa.php b/src/HestiaCP/Command/Dns/ChangeDnsDomainSoa.php new file mode 100644 index 0000000..1a36630 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsDomainSoa.php @@ -0,0 +1,34 @@ +positional([ + $this->user, + $this->domain, + $this->soa, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsDomainTpl.php b/src/HestiaCP/Command/Dns/ChangeDnsDomainTpl.php new file mode 100644 index 0000000..1901e03 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsDomainTpl.php @@ -0,0 +1,33 @@ +positional([ + $this->user, + $this->domain, + $this->template, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsDomainTtl.php b/src/HestiaCP/Command/Dns/ChangeDnsDomainTtl.php new file mode 100644 index 0000000..aeddba4 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsDomainTtl.php @@ -0,0 +1,34 @@ +positional([ + $this->user, + $this->domain, + (string) $this->ttl, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsRecord.php b/src/HestiaCP/Command/Dns/ChangeDnsRecord.php new file mode 100644 index 0000000..d9ef30a --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsRecord.php @@ -0,0 +1,47 @@ +positional([ + $this->user, + $this->domain, + (string) $this->id, + $this->record, + $this->type, + $this->value, + $this->priority === null ? null : (string) $this->priority, + $this->restart === null ? null : $this->convertBool($this->restart), + $this->ttl === null ? null : (string) $this->ttl + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ChangeDnsRecordId.php b/src/HestiaCP/Command/Dns/ChangeDnsRecordId.php new file mode 100644 index 0000000..8baea8f --- /dev/null +++ b/src/HestiaCP/Command/Dns/ChangeDnsRecordId.php @@ -0,0 +1,35 @@ +positional([ + $this->user, + $this->domain, + (string) $this->id, + (string) $this->newId, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/DeleteDnsDomain.php b/src/HestiaCP/Command/Dns/DeleteDnsDomain.php new file mode 100644 index 0000000..6a4de75 --- /dev/null +++ b/src/HestiaCP/Command/Dns/DeleteDnsDomain.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/DeleteDnsRecord.php b/src/HestiaCP/Command/Dns/DeleteDnsRecord.php new file mode 100644 index 0000000..b089df7 --- /dev/null +++ b/src/HestiaCP/Command/Dns/DeleteDnsRecord.php @@ -0,0 +1,33 @@ +positional([ + $this->user, + $this->domain, + (string) $this->id, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Dns/ListDnsDomain.php b/src/HestiaCP/Command/Dns/ListDnsDomain.php new file mode 100644 index 0000000..06f18c5 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ListDnsDomain.php @@ -0,0 +1,27 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Dns/ListDnsDomains.php b/src/HestiaCP/Command/Dns/ListDnsDomains.php new file mode 100644 index 0000000..9a89d81 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ListDnsDomains.php @@ -0,0 +1,22 @@ +positional([$this->user, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Dns/ListDnsRecords.php b/src/HestiaCP/Command/Dns/ListDnsRecords.php new file mode 100644 index 0000000..c9697ba --- /dev/null +++ b/src/HestiaCP/Command/Dns/ListDnsRecords.php @@ -0,0 +1,23 @@ +positional([$this->user, $this->domain, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Dns/ListDnsTemplate.php b/src/HestiaCP/Command/Dns/ListDnsTemplate.php new file mode 100644 index 0000000..81e684e --- /dev/null +++ b/src/HestiaCP/Command/Dns/ListDnsTemplate.php @@ -0,0 +1,22 @@ +positional([$this->template, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Dns/ListDnsTemplates.php b/src/HestiaCP/Command/Dns/ListDnsTemplates.php new file mode 100644 index 0000000..e46c50b --- /dev/null +++ b/src/HestiaCP/Command/Dns/ListDnsTemplates.php @@ -0,0 +1,18 @@ +positional([self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Dns/ListDnssecPublicKey.php b/src/HestiaCP/Command/Dns/ListDnssecPublicKey.php new file mode 100644 index 0000000..a570333 --- /dev/null +++ b/src/HestiaCP/Command/Dns/ListDnssecPublicKey.php @@ -0,0 +1,27 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Dns/SuspendDnsDomain.php b/src/HestiaCP/Command/Dns/SuspendDnsDomain.php new file mode 100644 index 0000000..eff0f94 --- /dev/null +++ b/src/HestiaCP/Command/Dns/SuspendDnsDomain.php @@ -0,0 +1,23 @@ +positional([$this->user, $this->domain]); + } +} diff --git a/src/HestiaCP/Command/Dns/SuspendDnsRecord.php b/src/HestiaCP/Command/Dns/SuspendDnsRecord.php new file mode 100644 index 0000000..8e6ed6c --- /dev/null +++ b/src/HestiaCP/Command/Dns/SuspendDnsRecord.php @@ -0,0 +1,24 @@ +positional([$this->user, $this->domain, (string) $this->id]); + } +} diff --git a/src/HestiaCP/Command/Dns/UnsuspendDnsDomain.php b/src/HestiaCP/Command/Dns/UnsuspendDnsDomain.php new file mode 100644 index 0000000..b97b4ee --- /dev/null +++ b/src/HestiaCP/Command/Dns/UnsuspendDnsDomain.php @@ -0,0 +1,23 @@ +positional([$this->user, $this->domain]); + } +} diff --git a/src/HestiaCP/Command/Dns/UnsuspendDnsRecord.php b/src/HestiaCP/Command/Dns/UnsuspendDnsRecord.php new file mode 100644 index 0000000..1cbcd77 --- /dev/null +++ b/src/HestiaCP/Command/Dns/UnsuspendDnsRecord.php @@ -0,0 +1,24 @@ +positional([$this->user, $this->domain, (string) $this->id]); + } +} diff --git a/src/HestiaCP/Command/Mail/AddMailAccountAlias.php b/src/HestiaCP/Command/Mail/AddMailAccountAlias.php new file mode 100644 index 0000000..04b3ace --- /dev/null +++ b/src/HestiaCP/Command/Mail/AddMailAccountAlias.php @@ -0,0 +1,30 @@ +positional([ + $this->user, + $this->domain, + $this->account, + $this->alias + ]); + } +} diff --git a/src/HestiaCP/Command/Mail/AddMailAccountAutoreply.php b/src/HestiaCP/Command/Mail/AddMailAccountAutoreply.php new file mode 100644 index 0000000..b6ee770 --- /dev/null +++ b/src/HestiaCP/Command/Mail/AddMailAccountAutoreply.php @@ -0,0 +1,30 @@ +positional([ + $this->user, + $this->domain, + $this->account, + $this->message + ]); + } +} diff --git a/src/HestiaCP/Command/Mail/AddMailAccountForward.php b/src/HestiaCP/Command/Mail/AddMailAccountForward.php new file mode 100644 index 0000000..b1ef7dd --- /dev/null +++ b/src/HestiaCP/Command/Mail/AddMailAccountForward.php @@ -0,0 +1,30 @@ +positional([ + $this->user, + $this->domain, + $this->account, + $this->forward + ]); + } +} diff --git a/src/HestiaCP/Command/Mail/AddMailAccountFwdOnly.php b/src/HestiaCP/Command/Mail/AddMailAccountFwdOnly.php new file mode 100644 index 0000000..b7ee968 --- /dev/null +++ b/src/HestiaCP/Command/Mail/AddMailAccountFwdOnly.php @@ -0,0 +1,24 @@ +positional([$this->user, $this->domain, $this->account]); + } +} diff --git a/src/HestiaCP/Command/Mail/DeleteMailAccountAlias.php b/src/HestiaCP/Command/Mail/DeleteMailAccountAlias.php new file mode 100644 index 0000000..a321c89 --- /dev/null +++ b/src/HestiaCP/Command/Mail/DeleteMailAccountAlias.php @@ -0,0 +1,30 @@ +positional([ + $this->user, + $this->domain, + $this->account, + $this->alias + ]); + } +} diff --git a/src/HestiaCP/Command/Mail/DeleteMailAccountAutoreply.php b/src/HestiaCP/Command/Mail/DeleteMailAccountAutoreply.php new file mode 100644 index 0000000..1049fcf --- /dev/null +++ b/src/HestiaCP/Command/Mail/DeleteMailAccountAutoreply.php @@ -0,0 +1,24 @@ +positional([$this->user, $this->domain, $this->account]); + } +} diff --git a/src/HestiaCP/Command/Mail/DeleteMailAccountForward.php b/src/HestiaCP/Command/Mail/DeleteMailAccountForward.php new file mode 100644 index 0000000..9fe5fa8 --- /dev/null +++ b/src/HestiaCP/Command/Mail/DeleteMailAccountForward.php @@ -0,0 +1,30 @@ +positional([ + $this->user, + $this->domain, + $this->account, + $this->forward + ]); + } +} diff --git a/src/HestiaCP/Command/Mail/DeleteMailAccountFwdOnly.php b/src/HestiaCP/Command/Mail/DeleteMailAccountFwdOnly.php new file mode 100644 index 0000000..dac2b8b --- /dev/null +++ b/src/HestiaCP/Command/Mail/DeleteMailAccountFwdOnly.php @@ -0,0 +1,24 @@ +positional([$this->user, $this->domain, $this->account]); + } +} diff --git a/src/HestiaCP/Command/Mail/ListMailAccount.php b/src/HestiaCP/Command/Mail/ListMailAccount.php new file mode 100644 index 0000000..ce7797f --- /dev/null +++ b/src/HestiaCP/Command/Mail/ListMailAccount.php @@ -0,0 +1,28 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, $this->account, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Mail/ListMailAccountAutoreply.php b/src/HestiaCP/Command/Mail/ListMailAccountAutoreply.php new file mode 100644 index 0000000..fe38c5a --- /dev/null +++ b/src/HestiaCP/Command/Mail/ListMailAccountAutoreply.php @@ -0,0 +1,28 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, $this->account, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Mail/ListMailDomain.php b/src/HestiaCP/Command/Mail/ListMailDomain.php new file mode 100644 index 0000000..7e686ed --- /dev/null +++ b/src/HestiaCP/Command/Mail/ListMailDomain.php @@ -0,0 +1,27 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Package/ListUserPackage.php b/src/HestiaCP/Command/Package/ListUserPackage.php new file mode 100644 index 0000000..bdae4dd --- /dev/null +++ b/src/HestiaCP/Command/Package/ListUserPackage.php @@ -0,0 +1,26 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->package, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Package/ListUserPackages.php b/src/HestiaCP/Command/Package/ListUserPackages.php new file mode 100644 index 0000000..6df981b --- /dev/null +++ b/src/HestiaCP/Command/Package/ListUserPackages.php @@ -0,0 +1,18 @@ +positional([self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/RawArguments.php b/src/HestiaCP/Command/RawArguments.php new file mode 100644 index 0000000..92e207a --- /dev/null +++ b/src/HestiaCP/Command/RawArguments.php @@ -0,0 +1,43 @@ + */ + private readonly array $args; + + /** + * @param string $name The script name, e.g. v-list-users + * @param array $args The script's arguments, in order; keys are ignored (see positional()) + * @throws InvalidArgumentException + */ + public function __construct(string $name, array $args = []) { + // HestiaCP's scripts are all named v- + if (!preg_match('~^v-[a-z0-9]+(-[a-z0-9]+)*$~', $name)) { + throw new InvalidArgumentException("Invalid command name '$name', expected a HestiaCP script name such as 'v-list-users'"); + } + + $this->name = $name; + $this->args = array_values($args); + } + + public function getName(): string { + return $this->name; + } + + public function getRequestParams(): array { + return $this->positional($this->args); + } +} diff --git a/src/HestiaCP/Command/RawCommand.php b/src/HestiaCP/Command/RawCommand.php new file mode 100644 index 0000000..7412991 --- /dev/null +++ b/src/HestiaCP/Command/RawCommand.php @@ -0,0 +1,16 @@ +positional([$this->user, $this->email]); + } +} diff --git a/src/HestiaCP/Command/User/ChangeUserPackage.php b/src/HestiaCP/Command/User/ChangeUserPackage.php new file mode 100644 index 0000000..9ff4265 --- /dev/null +++ b/src/HestiaCP/Command/User/ChangeUserPackage.php @@ -0,0 +1,28 @@ +positional([ + $this->user, + $this->package, + $this->convertBool($this->force) + ]); + } +} diff --git a/src/HestiaCP/Command/User/ListUserStats.php b/src/HestiaCP/Command/User/ListUserStats.php new file mode 100644 index 0000000..c01a698 --- /dev/null +++ b/src/HestiaCP/Command/User/ListUserStats.php @@ -0,0 +1,22 @@ +positional([$this->user, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Web/AddWebDomainAlias.php b/src/HestiaCP/Command/Web/AddWebDomainAlias.php new file mode 100644 index 0000000..b54261a --- /dev/null +++ b/src/HestiaCP/Command/Web/AddWebDomainAlias.php @@ -0,0 +1,34 @@ +positional([ + $this->user, + $this->domain, + $this->aliases, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/AddWebDomainProxy.php b/src/HestiaCP/Command/Web/AddWebDomainProxy.php new file mode 100644 index 0000000..fe8edd9 --- /dev/null +++ b/src/HestiaCP/Command/Web/AddWebDomainProxy.php @@ -0,0 +1,43 @@ +positional([ + $this->user, + $this->domain, + $this->template, + $this->extensions, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/AddWebDomainSsl.php b/src/HestiaCP/Command/Web/AddWebDomainSsl.php new file mode 100644 index 0000000..eec4689 --- /dev/null +++ b/src/HestiaCP/Command/Web/AddWebDomainSsl.php @@ -0,0 +1,53 @@ +.crt, .key and optionally .ca + * @param string|null $sslHome One of the SSL_HOME_* constants. HestiaCP uses same when it isn't set. + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @throws InvalidArgumentException + */ + public function __construct( + private readonly string $user, + private readonly string $domain, + private readonly string $sslDir, + private readonly ?string $sslHome = null, + private readonly ?bool $restart = null + ) { + if ($sslHome !== null && !in_array($sslHome, self::SSL_HOMES, true)) { + throw new InvalidArgumentException("Invalid SSL home '$sslHome', expected one of: " . implode(', ', self::SSL_HOMES)); + } + } + + public function getName(): string { + return 'v-add-web-domain-ssl'; + } + + public function getRequestParams(): array { + // HestiaCP stores an empty SSL home as is, so send its default when restart comes after it + $sslHome = $this->sslHome ?? ($this->restart === null ? null : self::SSL_HOME_SAME); + + return $this->positional([ + $this->user, + $this->domain, + $this->sslDir, + $sslHome, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/AddWebDomainSslForce.php b/src/HestiaCP/Command/Web/AddWebDomainSslForce.php new file mode 100644 index 0000000..b9d9b98 --- /dev/null +++ b/src/HestiaCP/Command/Web/AddWebDomainSslForce.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/AddWebDomainSslHsts.php b/src/HestiaCP/Command/Web/AddWebDomainSslHsts.php new file mode 100644 index 0000000..122f332 --- /dev/null +++ b/src/HestiaCP/Command/Web/AddWebDomainSslHsts.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/ChangeWebDomainBackendTpl.php b/src/HestiaCP/Command/Web/ChangeWebDomainBackendTpl.php new file mode 100644 index 0000000..8e7e736 --- /dev/null +++ b/src/HestiaCP/Command/Web/ChangeWebDomainBackendTpl.php @@ -0,0 +1,33 @@ +positional([ + $this->user, + $this->domain, + $this->template, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/ChangeWebDomainProxyTpl.php b/src/HestiaCP/Command/Web/ChangeWebDomainProxyTpl.php new file mode 100644 index 0000000..8f6014b --- /dev/null +++ b/src/HestiaCP/Command/Web/ChangeWebDomainProxyTpl.php @@ -0,0 +1,42 @@ +positional([ + $this->user, + $this->domain, + $this->template, + $this->extensions, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/ChangeWebDomainSslCert.php b/src/HestiaCP/Command/Web/ChangeWebDomainSslCert.php new file mode 100644 index 0000000..b443fc9 --- /dev/null +++ b/src/HestiaCP/Command/Web/ChangeWebDomainSslCert.php @@ -0,0 +1,34 @@ +.crt, .key and optionally .ca + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + */ + public function __construct( + private readonly string $user, + private readonly string $domain, + private readonly string $sslDir, + private readonly ?bool $restart = null + ) {} + + public function getName(): string { + return 'v-change-web-domain-sslcert'; + } + + public function getRequestParams(): array { + return $this->positional([ + $this->user, + $this->domain, + $this->sslDir, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/ChangeWebDomainSslHome.php b/src/HestiaCP/Command/Web/ChangeWebDomainSslHome.php new file mode 100644 index 0000000..36594f5 --- /dev/null +++ b/src/HestiaCP/Command/Web/ChangeWebDomainSslHome.php @@ -0,0 +1,42 @@ +positional([ + $this->user, + $this->domain, + $this->sslHome, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/ChangeWebDomainTpl.php b/src/HestiaCP/Command/Web/ChangeWebDomainTpl.php new file mode 100644 index 0000000..c27ab3f --- /dev/null +++ b/src/HestiaCP/Command/Web/ChangeWebDomainTpl.php @@ -0,0 +1,33 @@ +positional([ + $this->user, + $this->domain, + $this->template, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/DeleteWebDomainAlias.php b/src/HestiaCP/Command/Web/DeleteWebDomainAlias.php new file mode 100644 index 0000000..331c247 --- /dev/null +++ b/src/HestiaCP/Command/Web/DeleteWebDomainAlias.php @@ -0,0 +1,33 @@ +positional([ + $this->user, + $this->domain, + $this->alias, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/DeleteWebDomainProxy.php b/src/HestiaCP/Command/Web/DeleteWebDomainProxy.php new file mode 100644 index 0000000..4090786 --- /dev/null +++ b/src/HestiaCP/Command/Web/DeleteWebDomainProxy.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/DeleteWebDomainSsl.php b/src/HestiaCP/Command/Web/DeleteWebDomainSsl.php new file mode 100644 index 0000000..8774c70 --- /dev/null +++ b/src/HestiaCP/Command/Web/DeleteWebDomainSsl.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/DeleteWebDomainSslForce.php b/src/HestiaCP/Command/Web/DeleteWebDomainSslForce.php new file mode 100644 index 0000000..27f0f17 --- /dev/null +++ b/src/HestiaCP/Command/Web/DeleteWebDomainSslForce.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/DeleteWebDomainSslHsts.php b/src/HestiaCP/Command/Web/DeleteWebDomainSslHsts.php new file mode 100644 index 0000000..011e480 --- /dev/null +++ b/src/HestiaCP/Command/Web/DeleteWebDomainSslHsts.php @@ -0,0 +1,31 @@ +positional([ + $this->user, + $this->domain, + $this->restart === null ? null : $this->convertBool($this->restart) + ]); + } +} diff --git a/src/HestiaCP/Command/Web/ListWebDomain.php b/src/HestiaCP/Command/Web/ListWebDomain.php new file mode 100644 index 0000000..e99e009 --- /dev/null +++ b/src/HestiaCP/Command/Web/ListWebDomain.php @@ -0,0 +1,27 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Web/ListWebDomainSsl.php b/src/HestiaCP/Command/Web/ListWebDomainSsl.php new file mode 100644 index 0000000..9075896 --- /dev/null +++ b/src/HestiaCP/Command/Web/ListWebDomainSsl.php @@ -0,0 +1,27 @@ +convertDetail(parent::process()); + } + + public function getRequestParams(): array { + return $this->positional([$this->user, $this->domain, self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Web/ListWebTemplates.php b/src/HestiaCP/Command/Web/ListWebTemplates.php new file mode 100644 index 0000000..7966801 --- /dev/null +++ b/src/HestiaCP/Command/Web/ListWebTemplates.php @@ -0,0 +1,18 @@ +positional([self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Web/ListWebTemplatesBackend.php b/src/HestiaCP/Command/Web/ListWebTemplatesBackend.php new file mode 100644 index 0000000..976bc2a --- /dev/null +++ b/src/HestiaCP/Command/Web/ListWebTemplatesBackend.php @@ -0,0 +1,18 @@ +positional([self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Command/Web/ListWebTemplatesProxy.php b/src/HestiaCP/Command/Web/ListWebTemplatesProxy.php new file mode 100644 index 0000000..5661952 --- /dev/null +++ b/src/HestiaCP/Command/Web/ListWebTemplatesProxy.php @@ -0,0 +1,18 @@ +positional([self::FORMAT_JSON]); + } +} diff --git a/src/HestiaCP/Module/Crons.php b/src/HestiaCP/Module/Crons.php new file mode 100644 index 0000000..e0150fe --- /dev/null +++ b/src/HestiaCP/Module/Crons.php @@ -0,0 +1,123 @@ +user = $user; + } + + /** + * This function obtains the list of all user's cron jobs, keyed by job ID. + * + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listJobs(): array { + return $this->client->send(new ListCronJobs($this->user)); + } + + /** + * This function obtains the parameters of a cron job. + * + * @param int $job + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listJob(int $job): array { + return $this->client->send(new ListCronJob($this->user, $job)); + } + + /** + * This function adds a new cron job. + * + * @param string $min + * @param string $hour + * @param string $day + * @param string $month + * @param string $wday + * @param string $command + * @param int|null $job Job ID, or null for the next free ID + * @param bool|null $restart Restart the cron service, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addJob(string $min, string $hour, string $day, string $month, string $wday, string $command, ?int $job = null, ?bool $restart = null): bool { + return $this->client->send(new AddCronJob($this->user, $min, $hour, $day, $month, $wday, $command, $job, $restart)); + } + + /** + * This function changes the schedule and command of a cron job. + * + * @param int $job + * @param string $min + * @param string $hour + * @param string $day + * @param string $month + * @param string $wday + * @param string $command + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeJob(int $job, string $min, string $hour, string $day, string $month, string $wday, string $command): bool { + return $this->client->send(new ChangeCronJob($this->user, $job, $min, $hour, $day, $month, $wday, $command)); + } + + /** + * This function suspends a cron job. + * + * @param int $job + * @param bool|null $restart Restart the cron service, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function suspendJob(int $job, ?bool $restart = null): bool { + return $this->client->send(new SuspendCronJob($this->user, $job, $restart)); + } + + /** + * This function unsuspends a cron job. + * + * @param int $job + * @param bool|null $restart Restart the cron service, or null for HestiaCP's default (restart) + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function unsuspendJob(int $job, ?bool $restart = null): bool { + return $this->client->send(new UnsuspendCronJob($this->user, $job, $restart)); + } + + /** + * This function deletes a cron job. + * + * @param int $job + * @param bool|null $restart Restart the cron service, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteJob(int $job, ?bool $restart = null): bool { + return $this->client->send(new DeleteCronJob($this->user, $job, $restart)); + } +} diff --git a/src/HestiaCP/Module/Databases.php b/src/HestiaCP/Module/Databases.php index 75dc1a7..b68fd7a 100644 --- a/src/HestiaCP/Module/Databases.php +++ b/src/HestiaCP/Module/Databases.php @@ -6,10 +6,13 @@ use neto737\HestiaCP\Client; use neto737\HestiaCP\Command\Database\AddDatabase; +use neto737\HestiaCP\Command\Database\ChangeDatabasePassword; use neto737\HestiaCP\Command\Database\DeleteDatabase; use neto737\HestiaCP\Command\Database\DeleteDatabases; use neto737\HestiaCP\Command\Database\ListDatabase; use neto737\HestiaCP\Command\Database\ListDatabases; +use neto737\HestiaCP\Command\Database\SuspendDatabase; +use neto737\HestiaCP\Command\Database\UnsuspendDatabase; class Databases extends Module { @@ -35,6 +38,43 @@ public function add(string $database, string $dbuser, string $dbpass, string $ty return $this->client->send(new AddDatabase($this->user, $database, $dbuser, $dbpass, $type)); } + /** + * This function changes the password of a database's user. + * + * @param string $database + * @param string $dbpass + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changePassword(string $database, string $dbpass): bool { + return $this->client->send(new ChangeDatabasePassword($this->user, $database, $dbpass)); + } + + /** + * This function suspends a database. + * + * @param string $database + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function suspend(string $database): bool { + return $this->client->send(new SuspendDatabase($this->user, $database)); + } + + /** + * This function unsuspends a database. + * + * @param string $database + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function unsuspend(string $database): bool { + return $this->client->send(new UnsuspendDatabase($this->user, $database)); + } + /** * This function is used for delete a specified database. * diff --git a/src/HestiaCP/Module/Dns.php b/src/HestiaCP/Module/Dns.php new file mode 100644 index 0000000..e551a7d --- /dev/null +++ b/src/HestiaCP/Module/Dns.php @@ -0,0 +1,338 @@ +user = $user; + } + + /** + * This function obtains the list of all user's DNS domains. + * + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listDomains(): array { + return $this->client->send(new ListDnsDomains($this->user)); + } + + /** + * This function obtains the parameters of a DNS domain. + * + * @param string $domain + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listDomain(string $domain): array { + return $this->client->send(new ListDnsDomain($this->user, $domain)); + } + + /** + * This function adds a DNS domain, with the records of the user's DNS template. + * + * @param string $domain + * @param string $ip + * @param list $nameservers Two to eight nameservers, or none for the user's + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @param bool|null $dnssec Turn on DNSSEC, or null for HestiaCP's default (off) + * @return bool + * @throws \neto737\HestiaCP\Exceptions\InvalidArgumentException + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addDomain(string $domain, string $ip, array $nameservers = [], ?bool $restart = null, ?bool $dnssec = null): bool { + return $this->client->send(new AddDnsDomain($this->user, $domain, $ip, $nameservers, $restart, $dnssec)); + } + + /** + * This function deletes a DNS domain and all its records. + * + * @param string $domain + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteDomain(string $domain, ?bool $restart = null): bool { + return $this->client->send(new DeleteDnsDomain($this->user, $domain, $restart)); + } + + /** + * This function suspends a DNS domain. + * + * @param string $domain + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function suspendDomain(string $domain): bool { + return $this->client->send(new SuspendDnsDomain($this->user, $domain)); + } + + /** + * This function unsuspends a DNS domain. + * + * @param string $domain + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function unsuspendDomain(string $domain): bool { + return $this->client->send(new UnsuspendDnsDomain($this->user, $domain)); + } + + /** + * This function changes the IP address of a DNS domain, in the domain and in every record that holds it. + * + * @param string $domain + * @param string $ip + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainIp(string $domain, string $ip, ?bool $restart = null): bool { + return $this->client->send(new ChangeDnsDomainIp($this->user, $domain, $ip, $restart)); + } + + /** + * This function changes the TTL of a DNS domain. + * + * @param string $domain + * @param int $ttl TTL in seconds + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainTtl(string $domain, int $ttl, ?bool $restart = null): bool { + return $this->client->send(new ChangeDnsDomainTtl($this->user, $domain, $ttl, $restart)); + } + + /** + * This function changes the expiration date of a DNS domain. + * + * @param string $domain + * @param string $date YYYY-MM-DD + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainExpiration(string $domain, string $date): bool { + return $this->client->send(new ChangeDnsDomainExp($this->user, $domain, $date)); + } + + /** + * This function changes the primary nameserver in the SOA record of a DNS domain. + * + * @param string $domain + * @param string $soa + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainSoa(string $domain, string $soa, ?bool $restart = null): bool { + return $this->client->send(new ChangeDnsDomainSoa($this->user, $domain, $soa, $restart)); + } + + /** + * This function changes the template of a DNS domain and replaces its records with the template's. + * + * @param string $domain + * @param string $template + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainTemplate(string $domain, string $template, ?bool $restart = null): bool { + return $this->client->send(new ChangeDnsDomainTpl($this->user, $domain, $template, $restart)); + } + + /** + * This function turns DNSSEC on or off for a DNS domain. + * + * @param string $domain + * @param bool $enabled + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainDnssec(string $domain, bool $enabled): bool { + return $this->client->send(new ChangeDnsDomainDnssec($this->user, $domain, $enabled)); + } + + /** + * This function obtains the DNSSEC public key and DS record of a DNS domain. + * + * @param string $domain + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listDnssecPublicKey(string $domain): array { + return $this->client->send(new ListDnssecPublicKey($this->user, $domain)); + } + + /** + * This function obtains the records of a DNS domain, keyed by record ID. + * + * @param string $domain + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listRecords(string $domain): array { + return $this->client->send(new ListDnsRecords($this->user, $domain)); + } + + /** + * This function adds a DNS record. + * + * @param string $domain + * @param string $record Record name, e.g. www, or @ for the domain itself + * @param string $type Record type, e.g. A, AAAA, CNAME, MX, TXT, SRV, CAA + * @param string $value + * @param int|null $priority Priority of an MX or SRV record, or null for HestiaCP's default (10) + * @param int|null $id Record ID, or null for the next free ID + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @param int|null $ttl TTL in seconds, or null for the domain's TTL + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addRecord(string $domain, string $record, string $type, string $value, ?int $priority = null, ?int $id = null, ?bool $restart = null, ?int $ttl = null): bool { + return $this->client->send(new AddDnsRecord($this->user, $domain, $record, $type, $value, $priority, $id, $restart, $ttl)); + } + + /** + * This function replaces a DNS record. + * + * @param string $domain + * @param int $id + * @param string $record Record name, e.g. www, or @ for the domain itself + * @param string $type Record type, e.g. A, AAAA, CNAME, MX, TXT, SRV, CAA + * @param string $value + * @param int|null $priority Priority of an MX or SRV record, or null to keep the current one + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @param int|null $ttl TTL in seconds, or null for the domain's TTL + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeRecord(string $domain, int $id, string $record, string $type, string $value, ?int $priority = null, ?bool $restart = null, ?int $ttl = null): bool { + return $this->client->send(new ChangeDnsRecord($this->user, $domain, $id, $record, $type, $value, $priority, $restart, $ttl)); + } + + /** + * This function changes the ID of a DNS record. + * + * @param string $domain + * @param int $id + * @param int $newId + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeRecordId(string $domain, int $id, int $newId, ?bool $restart = null): bool { + return $this->client->send(new ChangeDnsRecordId($this->user, $domain, $id, $newId, $restart)); + } + + /** + * This function suspends a DNS record. + * + * @param string $domain + * @param int $id + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function suspendRecord(string $domain, int $id): bool { + return $this->client->send(new SuspendDnsRecord($this->user, $domain, $id)); + } + + /** + * This function unsuspends a DNS record. + * + * @param string $domain + * @param int $id + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function unsuspendRecord(string $domain, int $id): bool { + return $this->client->send(new UnsuspendDnsRecord($this->user, $domain, $id)); + } + + /** + * This function deletes a DNS record. + * + * @param string $domain + * @param int $id + * @param bool|null $restart Restart the DNS server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteRecord(string $domain, int $id, ?bool $restart = null): bool { + return $this->client->send(new DeleteDnsRecord($this->user, $domain, $id, $restart)); + } + + /** + * This function obtains the names of the DNS templates. + * + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listTemplates(): array { + return $this->client->send(new ListDnsTemplates()); + } + + /** + * This function obtains the records of a DNS template, keyed by record ID. + * + * @param string $template + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listTemplate(string $template): array { + return $this->client->send(new ListDnsTemplate($template)); + } +} diff --git a/src/HestiaCP/Module/Mails.php b/src/HestiaCP/Module/Mails.php index 629033a..fc49c47 100644 --- a/src/HestiaCP/Module/Mails.php +++ b/src/HestiaCP/Module/Mails.php @@ -6,6 +6,10 @@ use neto737\HestiaCP\Client; use neto737\HestiaCP\Command\Mail\AddMailAccount; +use neto737\HestiaCP\Command\Mail\AddMailAccountAlias; +use neto737\HestiaCP\Command\Mail\AddMailAccountAutoreply; +use neto737\HestiaCP\Command\Mail\AddMailAccountForward; +use neto737\HestiaCP\Command\Mail\AddMailAccountFwdOnly; use neto737\HestiaCP\Command\Mail\AddMailDomain; use neto737\HestiaCP\Command\Mail\ChangeMailAccountPassword; use neto737\HestiaCP\Command\Mail\SuspendMailAccount; @@ -13,8 +17,15 @@ use neto737\HestiaCP\Command\Mail\UnsuspendMailAccount; use neto737\HestiaCP\Command\Mail\UnsuspendMailDomain; use neto737\HestiaCP\Command\Mail\DeleteMailAccount; +use neto737\HestiaCP\Command\Mail\DeleteMailAccountAlias; +use neto737\HestiaCP\Command\Mail\DeleteMailAccountAutoreply; +use neto737\HestiaCP\Command\Mail\DeleteMailAccountForward; +use neto737\HestiaCP\Command\Mail\DeleteMailAccountFwdOnly; use neto737\HestiaCP\Command\Mail\DeleteMailDomain; +use neto737\HestiaCP\Command\Mail\ListMailAccount; +use neto737\HestiaCP\Command\Mail\ListMailAccountAutoreply; use neto737\HestiaCP\Command\Mail\ListMailAccounts; +use neto737\HestiaCP\Command\Mail\ListMailDomain; use neto737\HestiaCP\Command\Mail\ListMailDomainDkim; use neto737\HestiaCP\Command\Mail\ListMailDomainDkimDns; use neto737\HestiaCP\Command\Mail\ListMailDomains; @@ -192,4 +203,151 @@ public function listDomainDkimDns(string $domain): array { public function listDomains(): array { return $this->client->send(new ListMailDomains($this->user)); } + + /** + * This function obtains the parameters of a mail domain. + * + * @param string $domain + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listDomain(string $domain): array { + return $this->client->send(new ListMailDomain($this->user, $domain)); + } + + /** + * This function obtains the parameters of a mail account, including its aliases and forwards. + * + * @param string $domain + * @param string $account + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listAccount(string $domain, string $account): array { + return $this->client->send(new ListMailAccount($this->user, $domain, $account)); + } + + /** + * This function adds an alias to a mail account. + * + * @param string $domain + * @param string $account + * @param string $alias The part before the @, on the account's domain + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addAccountAlias(string $domain, string $account, string $alias): bool { + return $this->client->send(new AddMailAccountAlias($this->user, $domain, $account, $alias)); + } + + /** + * This function deletes an alias from a mail account. + * + * @param string $domain + * @param string $account + * @param string $alias The part before the @ + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteAccountAlias(string $domain, string $account, string $alias): bool { + return $this->client->send(new DeleteMailAccountAlias($this->user, $domain, $account, $alias)); + } + + /** + * This function adds a forwarding address to a mail account. + * + * @param string $domain + * @param string $account + * @param string $forward Full email address + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addAccountForward(string $domain, string $account, string $forward): bool { + return $this->client->send(new AddMailAccountForward($this->user, $domain, $account, $forward)); + } + + /** + * This function deletes a forwarding address from a mail account. + * + * @param string $domain + * @param string $account + * @param string $forward Full email address + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteAccountForward(string $domain, string $account, string $forward): bool { + return $this->client->send(new DeleteMailAccountForward($this->user, $domain, $account, $forward)); + } + + /** + * This function makes a mail account forward only: mail is forwarded and not kept in the mailbox. + * + * @param string $domain + * @param string $account + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addAccountForwardOnly(string $domain, string $account): bool { + return $this->client->send(new AddMailAccountFwdOnly($this->user, $domain, $account)); + } + + /** + * This function makes a mail account keep forwarded mail in the mailbox again. + * + * @param string $domain + * @param string $account + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteAccountForwardOnly(string $domain, string $account): bool { + return $this->client->send(new DeleteMailAccountFwdOnly($this->user, $domain, $account)); + } + + /** + * This function obtains the autoreply message of a mail account. + * + * @param string $domain + * @param string $account + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listAccountAutoreply(string $domain, string $account): array { + return $this->client->send(new ListMailAccountAutoreply($this->user, $domain, $account)); + } + + /** + * This function turns on the autoreply of a mail account, or replaces its message. + * + * @param string $domain + * @param string $account + * @param string $message + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addAccountAutoreply(string $domain, string $account, string $message): bool { + return $this->client->send(new AddMailAccountAutoreply($this->user, $domain, $account, $message)); + } + + /** + * This function turns off the autoreply of a mail account. + * + * @param string $domain + * @param string $account + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteAccountAutoreply(string $domain, string $account): bool { + return $this->client->send(new DeleteMailAccountAutoreply($this->user, $domain, $account)); + } } diff --git a/src/HestiaCP/Module/Packages.php b/src/HestiaCP/Module/Packages.php new file mode 100644 index 0000000..66d4735 --- /dev/null +++ b/src/HestiaCP/Module/Packages.php @@ -0,0 +1,34 @@ + + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function list(): array { + return $this->client->send(new ListUserPackages()); + } + + /** + * This function obtains the parameters of a hosting package. + * + * @param string $package + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function detail(string $package): array { + return $this->client->send(new ListUserPackage($package)); + } +} diff --git a/src/HestiaCP/Module/Users.php b/src/HestiaCP/Module/Users.php index 7ff8129..6228e0f 100644 --- a/src/HestiaCP/Module/Users.php +++ b/src/HestiaCP/Module/Users.php @@ -5,12 +5,15 @@ namespace neto737\HestiaCP\Module; use neto737\HestiaCP\Command\User\AddUser; +use neto737\HestiaCP\Command\User\ChangeUserContact; +use neto737\HestiaCP\Command\User\ChangeUserPackage; use neto737\HestiaCP\Command\User\ChangeUserPassword; use neto737\HestiaCP\Command\User\SuspendUser; use neto737\HestiaCP\Command\User\UnsuspendUser; use neto737\HestiaCP\Command\User\DeleteUser; use neto737\HestiaCP\Command\User\ListUser; use neto737\HestiaCP\Command\User\ListUsers; +use neto737\HestiaCP\Command\User\ListUserStats; use neto737\HestiaCP\Command\User\BackupUsers; class Users extends Module { @@ -38,6 +41,18 @@ public function detail(string $user): array { return $this->client->send(new ListUser($user)); } + /** + * This function obtains the monthly usage statistics of a user, keyed by date. + * + * @param string $user + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function stats(string $user): array { + return $this->client->send(new ListUserStats($user)); + } + /** * This function changes user's password and updates RKEY value. * @@ -51,6 +66,33 @@ public function changePassword(string $user, string $password): bool { return $this->client->send(new ChangeUserPassword($user, $password)); } + /** + * This function changes user's contact email. + * + * @param string $user + * @param string $email + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeContact(string $user, string $email): bool { + return $this->client->send(new ChangeUserContact($user, $email)); + } + + /** + * This function changes user's hosting package. + * + * @param string $user + * @param string $package + * @param bool $force Change the package even if the user's current usage exceeds its limits + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changePackage(string $user, string $package, bool $force = false): bool { + return $this->client->send(new ChangeUserPackage($user, $package, $force)); + } + /** * This function creates new user account. * diff --git a/src/HestiaCP/Module/Webs.php b/src/HestiaCP/Module/Webs.php index c1d5a13..65a7447 100644 --- a/src/HestiaCP/Module/Webs.php +++ b/src/HestiaCP/Module/Webs.php @@ -7,15 +7,35 @@ use neto737\HestiaCP\Client; use neto737\HestiaCP\Command\Web\AddLetsEncryptDomain; use neto737\HestiaCP\Command\Web\AddWebDomain; +use neto737\HestiaCP\Command\Web\AddWebDomainAlias; use neto737\HestiaCP\Command\Web\AddWebDomainFtp; +use neto737\HestiaCP\Command\Web\AddWebDomainProxy; +use neto737\HestiaCP\Command\Web\AddWebDomainSsl; +use neto737\HestiaCP\Command\Web\AddWebDomainSslForce; +use neto737\HestiaCP\Command\Web\AddWebDomainSslHsts; +use neto737\HestiaCP\Command\Web\ChangeWebDomainBackendTpl; use neto737\HestiaCP\Command\Web\ChangeWebDomainFtpPassword; use neto737\HestiaCP\Command\Web\ChangeWebDomainFtpPath; +use neto737\HestiaCP\Command\Web\ChangeWebDomainProxyTpl; +use neto737\HestiaCP\Command\Web\ChangeWebDomainSslCert; +use neto737\HestiaCP\Command\Web\ChangeWebDomainSslHome; +use neto737\HestiaCP\Command\Web\ChangeWebDomainTpl; use neto737\HestiaCP\Command\Web\SuspendWebDomain; use neto737\HestiaCP\Command\Web\UnsuspendWebDomain; use neto737\HestiaCP\Command\Web\DeleteLetsEncryptDomain; use neto737\HestiaCP\Command\Web\DeleteWebDomain; +use neto737\HestiaCP\Command\Web\DeleteWebDomainAlias; use neto737\HestiaCP\Command\Web\DeleteWebDomainFtp; +use neto737\HestiaCP\Command\Web\DeleteWebDomainProxy; +use neto737\HestiaCP\Command\Web\DeleteWebDomainSsl; +use neto737\HestiaCP\Command\Web\DeleteWebDomainSslForce; +use neto737\HestiaCP\Command\Web\DeleteWebDomainSslHsts; +use neto737\HestiaCP\Command\Web\ListWebDomain; +use neto737\HestiaCP\Command\Web\ListWebDomainSsl; use neto737\HestiaCP\Command\Web\ListWebDomains; +use neto737\HestiaCP\Command\Web\ListWebTemplates; +use neto737\HestiaCP\Command\Web\ListWebTemplatesBackend; +use neto737\HestiaCP\Command\Web\ListWebTemplatesProxy; class Webs extends Module { @@ -175,4 +195,272 @@ public function deleteDomainFtp(string $domain, string $ftpUser): bool { public function listDomains(): array { return $this->client->send(new ListWebDomains($this->user)); } + + /** + * This function obtains the parameters of a web domain. + * + * @param string $domain + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listDomain(string $domain): array { + return $this->client->send(new ListWebDomain($this->user, $domain)); + } + + /** + * This function adds one or more aliases to a web domain. + * + * @param string $domain + * @param string $aliases Comma-separated + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addDomainAlias(string $domain, string $aliases, ?bool $restart = null): bool { + return $this->client->send(new AddWebDomainAlias($this->user, $domain, $aliases, $restart)); + } + + /** + * This function deletes an alias from a web domain. + * + * @param string $domain + * @param string $alias + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteDomainAlias(string $domain, string $alias, ?bool $restart = null): bool { + return $this->client->send(new DeleteWebDomainAlias($this->user, $domain, $alias, $restart)); + } + + /** + * This function obtains the SSL certificate of a web domain. + * + * @param string $domain + * @return array + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listDomainSsl(string $domain): array { + return $this->client->send(new ListWebDomainSsl($this->user, $domain)); + } + + /** + * This function turns on SSL for a web domain with a certificate already on the server. + * + * @param string $domain + * @param string $sslDir Directory on the server holding .crt, .key and optionally .ca + * @param string|null $sslHome One of the AddWebDomainSsl::SSL_HOME_* constants, or null for HestiaCP's default (same) + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + * @throws \neto737\HestiaCP\Exceptions\InvalidArgumentException + */ + public function addDomainSsl(string $domain, string $sslDir, ?string $sslHome = null, ?bool $restart = null): bool { + return $this->client->send(new AddWebDomainSsl($this->user, $domain, $sslDir, $sslHome, $restart)); + } + + /** + * This function replaces the SSL certificate of a web domain with one already on the server. + * + * @param string $domain + * @param string $sslDir Directory on the server holding .crt, .key and optionally .ca + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainSslCert(string $domain, string $sslDir, ?bool $restart = null): bool { + return $this->client->send(new ChangeWebDomainSslCert($this->user, $domain, $sslDir, $restart)); + } + + /** + * This function changes the directory HTTPS serves for a web domain. + * + * @param string $domain + * @param string $sslHome One of the AddWebDomainSsl::SSL_HOME_* constants + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + * @throws \neto737\HestiaCP\Exceptions\InvalidArgumentException + */ + public function changeDomainSslHome(string $domain, string $sslHome, ?bool $restart = null): bool { + return $this->client->send(new ChangeWebDomainSslHome($this->user, $domain, $sslHome, $restart)); + } + + /** + * This function turns off SSL for a web domain and deletes its certificate. + * + * @param string $domain + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteDomainSsl(string $domain, ?bool $restart = null): bool { + return $this->client->send(new DeleteWebDomainSsl($this->user, $domain, $restart)); + } + + /** + * This function redirects HTTP to HTTPS for a web domain. + * + * @param string $domain + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addDomainSslForce(string $domain, ?bool $restart = null): bool { + return $this->client->send(new AddWebDomainSslForce($this->user, $domain, $restart)); + } + + /** + * This function stops redirecting HTTP to HTTPS for a web domain. + * + * @param string $domain + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteDomainSslForce(string $domain, ?bool $restart = null): bool { + return $this->client->send(new DeleteWebDomainSslForce($this->user, $domain, $restart)); + } + + /** + * This function turns on HSTS for a web domain. + * + * @param string $domain + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function addDomainSslHsts(string $domain, ?bool $restart = null): bool { + return $this->client->send(new AddWebDomainSslHsts($this->user, $domain, $restart)); + } + + /** + * This function turns off HSTS for a web domain. + * + * @param string $domain + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteDomainSslHsts(string $domain, ?bool $restart = null): bool { + return $this->client->send(new DeleteWebDomainSslHsts($this->user, $domain, $restart)); + } + + /** + * This function obtains the list of web server templates. + * + * @return array Template names + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listTemplates(): array { + return $this->client->send(new ListWebTemplates()); + } + + /** + * This function obtains the list of backend (PHP-FPM) templates. + * + * @return array Template names + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listBackendTemplates(): array { + return $this->client->send(new ListWebTemplatesBackend()); + } + + /** + * This function obtains the list of proxy templates. + * + * @return array Template names + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function listProxyTemplates(): array { + return $this->client->send(new ListWebTemplatesProxy()); + } + + /** + * This function changes the web server template of a web domain. + * + * @param string $domain + * @param string $template + * @param bool|null $restart Restart the web server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainTemplate(string $domain, string $template, ?bool $restart = null): bool { + return $this->client->send(new ChangeWebDomainTpl($this->user, $domain, $template, $restart)); + } + + /** + * This function changes the backend (PHP-FPM) template of a web domain. + * + * @param string $domain + * @param string $template + * @param bool|null $restart Restart the web server and backend, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function changeDomainBackendTemplate(string $domain, string $template, ?bool $restart = null): bool { + return $this->client->send(new ChangeWebDomainBackendTpl($this->user, $domain, $template, $restart)); + } + + /** + * This function changes the proxy template of a web domain. + * + * @param string $domain + * @param string $template + * @param string|null $extensions File extensions served by the proxy, comma-separated, or null for HestiaCP's default list + * @param bool|null $restart Restart the proxy server, or null for HestiaCP's default. Needs $extensions. + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + * @throws \neto737\HestiaCP\Exceptions\InvalidArgumentException + */ + public function changeDomainProxyTemplate(string $domain, string $template, ?string $extensions = null, ?bool $restart = null): bool { + return $this->client->send(new ChangeWebDomainProxyTpl($this->user, $domain, $template, $extensions, $restart)); + } + + /** + * This function turns on the proxy for a web domain. + * + * @param string $domain + * @param string|null $template Proxy template, or null for the user's package default + * @param string|null $extensions File extensions served by the proxy, comma-separated, or null for HestiaCP's default list + * @param bool|null $restart Restart the proxy server, or null for HestiaCP's default. Needs $extensions. + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + * @throws \neto737\HestiaCP\Exceptions\InvalidArgumentException + */ + public function addDomainProxy(string $domain, ?string $template = null, ?string $extensions = null, ?bool $restart = null): bool { + return $this->client->send(new AddWebDomainProxy($this->user, $domain, $template, $extensions, $restart)); + } + + /** + * This function turns off the proxy for a web domain. + * + * @param string $domain + * @param bool|null $restart Restart the proxy server, or null for HestiaCP's default + * @return bool + * @throws \neto737\HestiaCP\Exceptions\ClientException + * @throws \neto737\HestiaCP\Exceptions\ProcessException + */ + public function deleteDomainProxy(string $domain, ?bool $restart = null): bool { + return $this->client->send(new DeleteWebDomainProxy($this->user, $domain, $restart)); + } } diff --git a/tests/Command/ChangeTest.php b/tests/Command/ChangeTest.php index 62c4d80..aaf1880 100644 --- a/tests/Command/ChangeTest.php +++ b/tests/Command/ChangeTest.php @@ -8,6 +8,9 @@ use neto737\HestiaCP\Command\Mail\ChangeMailAccountPassword; use neto737\HestiaCP\Command\Web\ChangeWebDomainFtpPassword; use neto737\HestiaCP\Command\Web\ChangeWebDomainFtpPath; +use neto737\HestiaCP\Command\Database\ChangeDatabasePassword; +use neto737\HestiaCP\Command\User\ChangeUserContact; +use neto737\HestiaCP\Command\User\ChangeUserPackage; class ChangeTest extends \PHPUnit\Framework\TestCase { @@ -69,4 +72,45 @@ public function testWebDomainFtpPath(): void { $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } + + public function testDatabasePassword(): void { + $command = new ChangeDatabasePassword('admin', 'admin_db', 'pass'); + $this->assertSame('v-change-database-password', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'admin_db', + 'arg3' => 'pass' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testUserContact(): void { + $command = new ChangeUserContact('admin', 'admin@example.com'); + $this->assertSame('v-change-user-contact', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'admin@example.com'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testUserPackage(): void { + $command = new ChangeUserPackage('admin', 'gold'); + $this->assertSame('v-change-user-package', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'gold', + 'arg3' => 'no' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } } diff --git a/tests/Command/CronTest.php b/tests/Command/CronTest.php new file mode 100644 index 0000000..7fb2a00 --- /dev/null +++ b/tests/Command/CronTest.php @@ -0,0 +1,125 @@ +assertSame('v-list-cron-jobs', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + $this->assertSame([], $command->process()); + } + + public function testListCronJob(): void { + $command = new ListCronJob('admin', 7); + $this->assertSame('v-list-cron-job', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => '7', 'arg3' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddCronJob(): void { + $command = new AddCronJob('admin', '0', '3', '*', '*', '1-5', '/usr/bin/php cron.php'); + $this->assertSame('v-add-cron-job', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => '0', + 'arg3' => '3', + 'arg4' => '*', + 'arg5' => '*', + 'arg6' => '1-5', + 'arg7' => '/usr/bin/php cron.php' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddCronJobWithJobAndRestart(): void { + $command = new AddCronJob('admin', '0', '3', '*', '*', '*', 'uptime', 12, false); + $this->assertSame(['arg8' => '12', 'arg9' => 'no'], array_slice($command->getRequestParams(), 7)); + } + + public function testAddCronJobRestartWithoutJob(): void { + // an empty job ID makes HestiaCP pick the next free one + $command = new AddCronJob('admin', '0', '3', '*', '*', '*', 'uptime', null, true); + $this->assertSame(['arg8' => '', 'arg9' => 'yes'], array_slice($command->getRequestParams(), 7)); + } + + public function testChangeCronJob(): void { + $command = new ChangeCronJob('admin', 7, '*/5', '*', '*', '*', '*', 'uptime'); + $this->assertSame('v-change-cron-job', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => '7', + 'arg3' => '*/5', + 'arg4' => '*', + 'arg5' => '*', + 'arg6' => '*', + 'arg7' => '*', + 'arg8' => 'uptime' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testDeleteCronJob(): void { + $command = new DeleteCronJob('admin', 7); + $this->assertSame('v-delete-cron-job', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => '7'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testSuspendCronJob(): void { + $command = new SuspendCronJob('admin', 7); + $this->assertSame('v-suspend-cron-job', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => '7'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testUnsuspendCronJob(): void { + $command = new UnsuspendCronJob('admin', 7); + $this->assertSame('v-unsuspend-cron-job', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => '7'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testRestartIsSentWhenSet(): void { + $this->assertSame(['arg1' => 'admin', 'arg2' => '7', 'arg3' => 'yes'], (new DeleteCronJob('admin', 7, true))->getRequestParams()); + $this->assertSame(['arg1' => 'admin', 'arg2' => '7', 'arg3' => 'no'], (new SuspendCronJob('admin', 7, false))->getRequestParams()); + $this->assertSame(['arg1' => 'admin', 'arg2' => '7', 'arg3' => 'no'], (new UnsuspendCronJob('admin', 7, false))->getRequestParams()); + } +} diff --git a/tests/Command/DnsTest.php b/tests/Command/DnsTest.php new file mode 100644 index 0000000..10f957a --- /dev/null +++ b/tests/Command/DnsTest.php @@ -0,0 +1,220 @@ +assertSame('v-list-dns-domains', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + $this->assertSame([], $command->process()); + } + + public function testDomainDetailCommands(): void { + foreach ([ + 'v-list-dns-domain' => new ListDnsDomain('admin', 'example.com'), + 'v-list-dnssec-public-key' => new ListDnssecPublicKey('admin', 'example.com') + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'json'], $command->getRequestParams()); + $this->assertFalse($command->needReturnCode()); + + try { + $command->process(); + $this->fail($name . ' should throw on an empty response'); + } catch (\neto737\HestiaCP\Exceptions\InvalidResponseException) { + } + } + } + + public function testListDnsRecords(): void { + $command = new ListDnsRecords('admin', 'example.com'); + $this->assertSame('v-list-dns-records', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + $this->assertSame([], $command->process()); + } + + public function testListDnsTemplates(): void { + $command = new ListDnsTemplates(); + $this->assertSame('v-list-dns-templates', $command->getName()); + $this->assertSame(['arg1' => 'json'], $command->getRequestParams()); + + $command = new ListDnsTemplate('default'); + $this->assertSame('v-list-dns-template', $command->getName()); + $this->assertSame(['arg1' => 'default', 'arg2' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + $this->assertSame([], $command->process()); + } + + public function testAddDnsDomain(): void { + $command = new AddDnsDomain('admin', 'example.com', '192.0.2.10'); + $this->assertSame('v-add-dns-domain', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '192.0.2.10'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddDnsDomainWithNameservers(): void { + $command = new AddDnsDomain('admin', 'example.com', '192.0.2.10', ['ns1.example.com', 'ns2.example.com']); + $this->assertSame(['arg4' => 'ns1.example.com', 'arg5' => 'ns2.example.com'], array_slice($command->getRequestParams(), 3)); + } + + public function testAddDnsDomainRestartAndDnssecAfterNameservers(): void { + // the unused nameserver slots are sent empty, so restart and DNSSEC keep their positions + $command = new AddDnsDomain('admin', 'example.com', '192.0.2.10', ['ns1.example.com', 'ns2.example.com'], false, true); + $this->assertSame([ + 'arg4' => 'ns1.example.com', + 'arg5' => 'ns2.example.com', + 'arg6' => '', + 'arg7' => '', + 'arg8' => '', + 'arg9' => '', + 'arg10' => '', + 'arg11' => '', + 'arg12' => 'no', + 'arg13' => 'yes' + ], array_slice($command->getRequestParams(), 3)); + } + + public function testAddDnsDomainSingleNameserver(): void { + $this->expectException(InvalidArgumentException::class); + new AddDnsDomain('admin', 'example.com', '192.0.2.10', ['ns1.example.com']); + } + + public function testAddDnsDomainTooManyNameservers(): void { + $this->expectException(InvalidArgumentException::class); + new AddDnsDomain('admin', 'example.com', '192.0.2.10', array_map(fn (int $i) => 'ns' . $i . '.example.com', range(1, 9))); + } + + public function testDomainCommands(): void { + foreach ([ + 'v-delete-dns-domain' => new DeleteDnsDomain('admin', 'example.com'), + 'v-suspend-dns-domain' => new SuspendDnsDomain('admin', 'example.com'), + 'v-unsuspend-dns-domain' => new UnsuspendDnsDomain('admin', 'example.com') + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com'], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + } + + public function testChangeDomainCommands(): void { + foreach ([ + 'v-change-dns-domain-ip' => [new ChangeDnsDomainIp('admin', 'example.com', '192.0.2.20'), '192.0.2.20'], + 'v-change-dns-domain-ttl' => [new ChangeDnsDomainTtl('admin', 'example.com', 3600), '3600'], + 'v-change-dns-domain-exp' => [new ChangeDnsDomainExp('admin', 'example.com', '2027-10-18'), '2027-10-18'], + 'v-change-dns-domain-soa' => [new ChangeDnsDomainSoa('admin', 'example.com', 'ns1.example.com'), 'ns1.example.com'], + 'v-change-dns-domain-tpl' => [new ChangeDnsDomainTpl('admin', 'example.com', 'gmail'), 'gmail'], + 'v-change-dns-domain-dnssec' => [new ChangeDnsDomainDnssec('admin', 'example.com', true), 'yes'] + ] as $name => [$command, $value]) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => $value], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + + $this->assertSame('no', (new ChangeDnsDomainDnssec('admin', 'example.com', false))->getRequestParams()['arg3']); + } + + public function testAddDnsRecord(): void { + $command = new AddDnsRecord('admin', 'example.com', 'www', 'A', '192.0.2.10'); + $this->assertSame('v-add-dns-record', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'example.com', + 'arg3' => 'www', + 'arg4' => 'A', + 'arg5' => '192.0.2.10' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddDnsRecordTtlOnly(): void { + // empty priority and ID make HestiaCP use its default priority and the next free ID + $command = new AddDnsRecord('admin', 'example.com', 'www', 'A', '192.0.2.10', null, null, null, 300); + $this->assertSame(['arg6' => '', 'arg7' => '', 'arg8' => '', 'arg9' => '300'], array_slice($command->getRequestParams(), 5)); + } + + public function testChangeDnsRecord(): void { + $command = new ChangeDnsRecord('admin', 'example.com', 3, '@', 'MX', 'mail.example.com', 20); + $this->assertSame('v-change-dns-record', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'example.com', + 'arg3' => '3', + 'arg4' => '@', + 'arg5' => 'MX', + 'arg6' => 'mail.example.com', + 'arg7' => '20' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testChangeDnsRecordId(): void { + $command = new ChangeDnsRecordId('admin', 'example.com', 3, 30); + $this->assertSame('v-change-dns-record-id', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '3', 'arg4' => '30'], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + + public function testRecordCommands(): void { + foreach ([ + 'v-delete-dns-record' => new DeleteDnsRecord('admin', 'example.com', 3), + 'v-suspend-dns-record' => new SuspendDnsRecord('admin', 'example.com', 3), + 'v-unsuspend-dns-record' => new UnsuspendDnsRecord('admin', 'example.com', 3) + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '3'], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + } + + public function testRestartIsSentWhenSet(): void { + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'yes'], (new DeleteDnsDomain('admin', 'example.com', true))->getRequestParams()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '3', 'arg4' => 'no'], (new DeleteDnsRecord('admin', 'example.com', 3, false))->getRequestParams()); + $this->assertSame('no', (new ChangeDnsDomainTtl('admin', 'example.com', 3600, false))->getRequestParams()['arg4']); + $this->assertSame('yes', (new ChangeDnsRecordId('admin', 'example.com', 3, 30, true))->getRequestParams()['arg5']); + } +} diff --git a/tests/Command/ListsTest.php b/tests/Command/ListsTest.php index 196fcd6..8e63ebc 100644 --- a/tests/Command/ListsTest.php +++ b/tests/Command/ListsTest.php @@ -16,6 +16,9 @@ use neto737\HestiaCP\Command\Backup\ListUserBackupExclusions; use neto737\HestiaCP\Command\Database\ListDatabase; use neto737\HestiaCP\Command\Database\ListDatabases; +use neto737\HestiaCP\Command\User\ListUserStats; +use neto737\HestiaCP\Command\Package\ListUserPackage; +use neto737\HestiaCP\Command\Package\ListUserPackages; class ListsTest extends \PHPUnit\Framework\TestCase { @@ -119,4 +122,29 @@ public function testListDatabases(): void { $this->assertSame([], $command->process()); } + + public function testUserStats(): void { + $command = new ListUserStats('admin'); + $this->assertSame('v-list-user-stats', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); + + $this->assertSame([], $command->process()); + } + + public function testUserPackages(): void { + $command = new ListUserPackages(); + $this->assertSame('v-list-user-packages', $command->getName()); + $this->assertSame(['arg1' => 'json'], $command->getRequestParams()); + + $this->assertSame([], $command->process()); + } + + public function testUserPackage(): void { + $command = new ListUserPackage('default'); + $this->assertSame('v-list-user-package', $command->getName()); + $this->assertSame(['arg1' => 'default', 'arg2' => 'json'], $command->getRequestParams()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } } diff --git a/tests/Command/MailTest.php b/tests/Command/MailTest.php new file mode 100644 index 0000000..215807f --- /dev/null +++ b/tests/Command/MailTest.php @@ -0,0 +1,81 @@ +assertSame('v-list-mail-domain', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testListMailAccountCommands(): void { + foreach ([ + 'v-list-mail-account' => new ListMailAccount('admin', 'example.com', 'info'), + 'v-list-mail-account-autoreply' => new ListMailAccountAutoreply('admin', 'example.com', 'info') + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + + try { + $command->process(); + $this->fail('Expected an empty response to throw'); + } catch (\neto737\HestiaCP\Exceptions\InvalidResponseException $e) { + $this->assertSame('Empty response', $e->getMessage()); + } + } + } + + public function testAccountValueCommands(): void { + foreach ([ + 'v-add-mail-account-alias' => [new AddMailAccountAlias('admin', 'example.com', 'info', 'sales'), 'sales'], + 'v-delete-mail-account-alias' => [new DeleteMailAccountAlias('admin', 'example.com', 'info', 'sales'), 'sales'], + 'v-add-mail-account-forward' => [new AddMailAccountForward('admin', 'example.com', 'info', 'bob@example.org'), 'bob@example.org'], + 'v-delete-mail-account-forward' => [new DeleteMailAccountForward('admin', 'example.com', 'info', 'bob@example.org'), 'bob@example.org'], + 'v-add-mail-account-autoreply' => [new AddMailAccountAutoreply('admin', 'example.com', 'info', "Away until Monday.\nThanks"), "Away until Monday.\nThanks"] + ] as $name => [$command, $value]) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => $value], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + } + + public function testAccountOnlyCommands(): void { + foreach ([ + 'v-add-mail-account-fwd-only' => new AddMailAccountFwdOnly('admin', 'example.com', 'info'), + 'v-delete-mail-account-fwd-only' => new DeleteMailAccountFwdOnly('admin', 'example.com', 'info'), + 'v-delete-mail-account-autoreply' => new DeleteMailAccountAutoreply('admin', 'example.com', 'info') + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'info'], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + } + + public function testProcessWithoutResponse(): void { + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + (new AddMailAccountAlias('admin', 'example.com', 'info', 'sales'))->process(); + } +} diff --git a/tests/Command/RawCommandTest.php b/tests/Command/RawCommandTest.php new file mode 100644 index 0000000..54bafdd --- /dev/null +++ b/tests/Command/RawCommandTest.php @@ -0,0 +1,158 @@ +} + */ +class RawCommandTest extends \PHPUnit\Framework\TestCase { + + /** @var array */ + private array $history = []; + + private function createClient(Response $response): Client { + $this->history = []; + $stack = HandlerStack::create(new MockHandler([$response])); + $stack->push(Middleware::history($this->history)); + + return Client::simpleFactory('https://someHost', 'ACCESS:SECRET', null, ['handler' => $stack]); + } + + /** + * @return array + */ + private function sentParams(): array { + $this->assertCount(1, $this->history); + parse_str((string) $this->history[0]['request']->getBody(), $params); + + return $params; + } + + public function testRawCommand(): void { + $command = new RawCommand('v-add-cron-job', ['admin', '0', '3', '*', '*', '*', 'echo hi']); + $this->assertSame('v-add-cron-job', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => '0', + 'arg3' => '3', + 'arg4' => '*', + 'arg5' => '*', + 'arg6' => '*', + 'arg7' => 'echo hi' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + } + + public function testRawListCommand(): void { + $command = new RawListCommand('v-list-cron-jobs', ['admin', 'json']); + $this->assertSame('v-list-cron-jobs', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + } + + public function testWithoutArguments(): void { + $this->assertSame([], (new RawCommand('v-update-sys-queue'))->getRequestParams()); + $this->assertSame([], (new RawListCommand('v-list-sys-info'))->getRequestParams()); + } + + public function testNullArguments(): void { + $command = new RawCommand('v-add-dns-record', ['admin', 'example.com', 'www', 'A', '10.0.0.1', null, null, 'yes', null]); + + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'example.com', + 'arg3' => 'www', + 'arg4' => 'A', + 'arg5' => '10.0.0.1', + 'arg6' => '', + 'arg7' => '', + 'arg8' => 'yes' + ], $command->getRequestParams()); + } + + public function testArgumentKeysAreIgnored(): void { + $command = new RawCommand('v-suspend-database', ['user' => 'admin', 'database' => 'admin_db']); + + $this->assertSame(['arg1' => 'admin', 'arg2' => 'admin_db'], $command->getRequestParams()); + } + + /** + * @return iterable + */ + public static function invalidNameProvider(): iterable { + yield 'empty' => ['']; + yield 'no prefix' => ['list-users']; + yield 'prefix only' => ['v-']; + yield 'uppercase' => ['v-List-Users']; + yield 'trailing dash' => ['v-list-users-']; + yield 'double dash' => ['v-list--users']; + yield 'space' => ['v-list-users json']; + yield 'path' => ['../v-list-users']; + } + + #[DataProvider('invalidNameProvider')] + public function testInvalidNameThrows(string $name): void { + $this->expectException(InvalidArgumentException::class); + new RawCommand($name); + } + + #[DataProvider('invalidNameProvider')] + public function testInvalidListNameThrows(string $name): void { + $this->expectException(InvalidArgumentException::class); + new RawListCommand($name); + } + + public function testSendRawCommand(): void { + $client = $this->createClient(new Response(200, ['Hestia-Exit-Code' => '0'], '0')); + + $this->assertTrue($client->send(new RawCommand('v-suspend-database', ['admin', 'admin_db']))); + $this->assertSame([ + 'access_key' => 'ACCESS', + 'secret_key' => 'SECRET', + 'arg1' => 'admin', + 'arg2' => 'admin_db', + 'returncode' => 'yes', + 'cmd' => 'v-suspend-database' + ], $this->sentParams()); + } + + public function testSendRawCommandFailure(): void { + $client = $this->createClient(new Response(400, ['Hestia-Exit-Code' => '3'], '3')); + + $this->expectException(ProcessException::class); + $this->expectExceptionCode(ResponseCode::E_NOTEXIST); + $client->send(new RawCommand('v-suspend-database', ['admin', 'admin_db'])); + } + + public function testSendRawListCommand(): void { + $client = $this->createClient(new Response(200, ['Hestia-Exit-Code' => '0'], '{"1": {"MIN": "0", "HOUR": "3"}}')); + + $this->assertSame(['1' => ['MIN' => '0', 'HOUR' => '3']], $client->send(new RawListCommand('v-list-cron-jobs', ['admin', 'json']))); + $this->assertSame([ + 'access_key' => 'ACCESS', + 'secret_key' => 'SECRET', + 'arg1' => 'admin', + 'arg2' => 'json', + 'returncode' => 'no', + 'cmd' => 'v-list-cron-jobs' + ], $this->sentParams()); + } +} diff --git a/tests/Command/SuspendTest.php b/tests/Command/SuspendTest.php index 66bf307..5d792d5 100644 --- a/tests/Command/SuspendTest.php +++ b/tests/Command/SuspendTest.php @@ -8,6 +8,7 @@ use neto737\HestiaCP\Command\Web\SuspendWebDomain; use neto737\HestiaCP\Command\Mail\SuspendMailAccount; use neto737\HestiaCP\Command\Mail\SuspendMailDomain; +use neto737\HestiaCP\Command\Database\SuspendDatabase; class SuspendTest extends \PHPUnit\Framework\TestCase { @@ -65,4 +66,18 @@ public function testMailDomain(): void { $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } + + public function testDatabase(): void { + $command = new SuspendDatabase('admin', 'admin_db'); + $this->assertSame('v-suspend-database', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'admin_db' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } } diff --git a/tests/Command/UnsuspendTest.php b/tests/Command/UnsuspendTest.php index d592596..c5f28c2 100644 --- a/tests/Command/UnsuspendTest.php +++ b/tests/Command/UnsuspendTest.php @@ -8,6 +8,7 @@ use neto737\HestiaCP\Command\Web\UnsuspendWebDomain; use neto737\HestiaCP\Command\Mail\UnsuspendMailAccount; use neto737\HestiaCP\Command\Mail\UnsuspendMailDomain; +use neto737\HestiaCP\Command\Database\UnsuspendDatabase; class UnsuspendTest extends \PHPUnit\Framework\TestCase { @@ -65,4 +66,18 @@ public function testMailDomain(): void { $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); $command->process(); } + + public function testDatabase(): void { + $command = new UnsuspendDatabase('admin', 'admin_db'); + $this->assertSame('v-unsuspend-database', $command->getName()); + $this->assertSame([ + 'arg1' => 'admin', + 'arg2' => 'admin_db' + ], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } } diff --git a/tests/Command/WebTest.php b/tests/Command/WebTest.php new file mode 100644 index 0000000..6b481b3 --- /dev/null +++ b/tests/Command/WebTest.php @@ -0,0 +1,228 @@ +assertSame('v-list-web-domain', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testListWebDomainSsl(): void { + $command = new ListWebDomainSsl('admin', 'example.com'); + $this->assertSame('v-list-web-domain-ssl', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testListWebTemplates(): void { + foreach ([ + 'v-list-web-templates' => new ListWebTemplates(), + 'v-list-web-templates-backend' => new ListWebTemplatesBackend(), + 'v-list-web-templates-proxy' => new ListWebTemplatesProxy() + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'json'], $command->getRequestParams()); + + $this->assertFalse($command->needReturnCode()); + $this->assertSame([], $command->process()); + } + } + + public function testAddWebDomainAlias(): void { + $command = new AddWebDomainAlias('admin', 'example.com', 'www.example.com,shop.example.com'); + $this->assertSame('v-add-web-domain-alias', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'www.example.com,shop.example.com'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testDeleteWebDomainAlias(): void { + $command = new DeleteWebDomainAlias('admin', 'example.com', 'www.example.com'); + $this->assertSame('v-delete-web-domain-alias', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'www.example.com'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddWebDomainSsl(): void { + $command = new AddWebDomainSsl('admin', 'example.com', '/tmp/ssl'); + $this->assertSame('v-add-web-domain-ssl', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '/tmp/ssl'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddWebDomainSslRestartWithoutSslHome(): void { + // HestiaCP would store an empty SSL home, so its default is sent instead + $command = new AddWebDomainSsl('admin', 'example.com', '/tmp/ssl', null, false); + $this->assertSame(['arg4' => 'same', 'arg5' => 'no'], array_slice($command->getRequestParams(), 3)); + } + + public function testAddWebDomainSslInvalidSslHome(): void { + $this->expectException(InvalidArgumentException::class); + new AddWebDomainSsl('admin', 'example.com', '/tmp/ssl', 'public_shtml'); + } + + public function testChangeWebDomainSslCert(): void { + $command = new ChangeWebDomainSslCert('admin', 'example.com', '/tmp/ssl'); + $this->assertSame('v-change-web-domain-sslcert', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '/tmp/ssl'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testChangeWebDomainSslHome(): void { + $command = new ChangeWebDomainSslHome('admin', 'example.com', AddWebDomainSsl::SSL_HOME_SINGLE); + $this->assertSame('v-change-web-domain-sslhome', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'single'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testChangeWebDomainSslHomeInvalid(): void { + $this->expectException(InvalidArgumentException::class); + new ChangeWebDomainSslHome('admin', 'example.com', 'other'); + } + + public function testDomainOnlyCommands(): void { + foreach ([ + 'v-delete-web-domain-ssl' => new DeleteWebDomainSsl('admin', 'example.com'), + 'v-add-web-domain-ssl-force' => new AddWebDomainSslForce('admin', 'example.com'), + 'v-delete-web-domain-ssl-force' => new DeleteWebDomainSslForce('admin', 'example.com'), + 'v-add-web-domain-ssl-hsts' => new AddWebDomainSslHsts('admin', 'example.com'), + 'v-delete-web-domain-ssl-hsts' => new DeleteWebDomainSslHsts('admin', 'example.com'), + 'v-delete-web-domain-proxy' => new DeleteWebDomainProxy('admin', 'example.com') + ] as $name => $command) { + $this->assertSame($name, $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com'], $command->getRequestParams()); + $this->assertTrue($command->needReturnCode()); + } + } + + public function testChangeWebDomainTpl(): void { + $command = new ChangeWebDomainTpl('admin', 'example.com', 'hosting'); + $this->assertSame('v-change-web-domain-tpl', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'hosting'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testChangeWebDomainBackendTpl(): void { + $command = new ChangeWebDomainBackendTpl('admin', 'example.com', 'PHP-8_3'); + $this->assertSame('v-change-web-domain-backend-tpl', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'PHP-8_3'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testChangeWebDomainProxyTpl(): void { + $command = new ChangeWebDomainProxyTpl('admin', 'example.com', 'caching'); + $this->assertSame('v-change-web-domain-proxy-tpl', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'caching'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddWebDomainProxy(): void { + $command = new AddWebDomainProxy('admin', 'example.com'); + $this->assertSame('v-add-web-domain-proxy', $command->getName()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com'], $command->getRequestParams()); + + $this->assertTrue($command->needReturnCode()); + + $this->expectException(\neto737\HestiaCP\Exceptions\InvalidResponseException::class); + $command->process(); + } + + public function testAddWebDomainProxyExtensionsWithoutTemplate(): void { + // an empty template makes HestiaCP use the user's package default + $command = new AddWebDomainProxy('admin', 'example.com', null, 'jpg,png'); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => '', 'arg4' => 'jpg,png'], $command->getRequestParams()); + } + + public function testProxyRestartWithEmptyExtensions(): void { + // no extensions is still allowed, it just has to be asked for + $command = new AddWebDomainProxy('admin', 'example.com', null, '', true); + $this->assertSame(['arg3' => '', 'arg4' => '', 'arg5' => 'yes'], array_slice($command->getRequestParams(), 2)); + } + + public function testAddWebDomainProxyRestartNeedsExtensions(): void { + // HestiaCP reads an empty extensions argument as no extensions, not as its default list + $this->expectException(InvalidArgumentException::class); + new AddWebDomainProxy('admin', 'example.com', 'caching', null, false); + } + + public function testChangeWebDomainProxyTplRestartNeedsExtensions(): void { + $this->expectException(InvalidArgumentException::class); + new ChangeWebDomainProxyTpl('admin', 'example.com', 'caching', null, true); + } + + public function testRestartIsSentWhenSet(): void { + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'no'], (new DeleteWebDomainSsl('admin', 'example.com', false))->getRequestParams()); + $this->assertSame(['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'yes'], (new AddWebDomainSslForce('admin', 'example.com', true))->getRequestParams()); + $this->assertSame( + ['arg1' => 'admin', 'arg2' => 'example.com', 'arg3' => 'www.example.com', 'arg4' => 'no'], + (new DeleteWebDomainAlias('admin', 'example.com', 'www.example.com', false))->getRequestParams() + ); + } +} diff --git a/tests/Module/ModuleSendTest.php b/tests/Module/ModuleSendTest.php index 8d311d3..61f29c0 100644 --- a/tests/Module/ModuleSendTest.php +++ b/tests/Module/ModuleSendTest.php @@ -10,8 +10,11 @@ use GuzzleHttp\Psr7\Response; use neto737\HestiaCP\Client; use neto737\HestiaCP\Module\Backups; +use neto737\HestiaCP\Module\Crons; use neto737\HestiaCP\Module\Databases; +use neto737\HestiaCP\Module\Dns; use neto737\HestiaCP\Module\Mails; +use neto737\HestiaCP\Module\Packages; use neto737\HestiaCP\Module\Users; use neto737\HestiaCP\Module\Webs; use PHPUnit\Framework\Attributes\DataProvider; @@ -67,6 +70,16 @@ public static function processProvider(): iterable { 'v-change-user-password', ['arg1' => 'bob', 'arg2' => 'secret'] ]; + yield 'Users::changeContact' => [ + fn (Client $c) => $c->getModuleUser()->changeContact('bob', 'bob@example.com'), + 'v-change-user-contact', + ['arg1' => 'bob', 'arg2' => 'bob@example.com'] + ]; + yield 'Users::changePackage' => [ + fn (Client $c) => $c->getModuleUser()->changePackage('bob', 'gold', true), + 'v-change-user-package', + ['arg1' => 'bob', 'arg2' => 'gold', 'arg3' => 'yes'] + ]; yield 'Users::suspend' => [ fn (Client $c) => $c->getModuleUser()->suspend('bob', true), 'v-suspend-user', @@ -109,6 +122,21 @@ public static function processProvider(): iterable { 'v-add-database', ['arg1' => 'alice', 'arg2' => 'alice_db', 'arg3' => 'alice_dbuser', 'arg4' => 'secret', 'arg5' => 'pgsql'] ]; + yield 'Databases::changePassword' => [ + fn (Client $c) => $c->getModuleDatabase('alice')->changePassword('alice_db', 'secret'), + 'v-change-database-password', + ['arg1' => 'alice', 'arg2' => 'alice_db', 'arg3' => 'secret'] + ]; + yield 'Databases::suspend' => [ + fn (Client $c) => $c->getModuleDatabase('alice')->suspend('alice_db'), + 'v-suspend-database', + ['arg1' => 'alice', 'arg2' => 'alice_db'] + ]; + yield 'Databases::unsuspend' => [ + fn (Client $c) => $c->getModuleDatabase('alice')->unsuspend('alice_db'), + 'v-unsuspend-database', + ['arg1' => 'alice', 'arg2' => 'alice_db'] + ]; yield 'Databases::delete' => [ fn (Client $c) => $c->getModuleDatabase('alice')->delete('alice_db'), 'v-delete-database', @@ -120,6 +148,113 @@ public static function processProvider(): iterable { ['arg1' => 'alice'] ]; + yield 'Crons::addJob' => [ + fn (Client $c) => $c->getModuleCron('alice')->addJob('0', '3', '1', '2', '1-5', 'uptime', 12, true), + 'v-add-cron-job', + ['arg1' => 'alice', 'arg2' => '0', 'arg3' => '3', 'arg4' => '1', 'arg5' => '2', 'arg6' => '1-5', 'arg7' => 'uptime', 'arg8' => '12', 'arg9' => 'yes'] + ]; + yield 'Crons::changeJob' => [ + fn (Client $c) => $c->getModuleCron('alice')->changeJob(12, '0', '3', '1', '2', '1-5', 'uptime'), + 'v-change-cron-job', + ['arg1' => 'alice', 'arg2' => '12', 'arg3' => '0', 'arg4' => '3', 'arg5' => '1', 'arg6' => '2', 'arg7' => '1-5', 'arg8' => 'uptime'] + ]; + yield 'Crons::suspendJob' => [ + fn (Client $c) => $c->getModuleCron('alice')->suspendJob(12, true), + 'v-suspend-cron-job', + ['arg1' => 'alice', 'arg2' => '12', 'arg3' => 'yes'] + ]; + yield 'Crons::unsuspendJob' => [ + fn (Client $c) => $c->getModuleCron('alice')->unsuspendJob(12, false), + 'v-unsuspend-cron-job', + ['arg1' => 'alice', 'arg2' => '12', 'arg3' => 'no'] + ]; + yield 'Crons::deleteJob' => [ + fn (Client $c) => $c->getModuleCron('alice')->deleteJob(12, true), + 'v-delete-cron-job', + ['arg1' => 'alice', 'arg2' => '12', 'arg3' => 'yes'] + ]; + + yield 'Dns::addDomain' => [ + fn (Client $c) => $c->getModuleDns('alice')->addDomain('example.com', '192.0.2.10', ['ns1.example.org', 'ns2.example.org', 'ns3.example.org'], true, true), + 'v-add-dns-domain', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '192.0.2.10', 'arg4' => 'ns1.example.org', 'arg5' => 'ns2.example.org', 'arg6' => 'ns3.example.org', 'arg7' => '', 'arg8' => '', 'arg9' => '', 'arg10' => '', 'arg11' => '', 'arg12' => 'yes', 'arg13' => 'yes'] + ]; + yield 'Dns::deleteDomain' => [ + fn (Client $c) => $c->getModuleDns('alice')->deleteDomain('example.com', true), + 'v-delete-dns-domain', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Dns::suspendDomain' => [ + fn (Client $c) => $c->getModuleDns('alice')->suspendDomain('example.com'), + 'v-suspend-dns-domain', + ['arg1' => 'alice', 'arg2' => 'example.com'] + ]; + yield 'Dns::unsuspendDomain' => [ + fn (Client $c) => $c->getModuleDns('alice')->unsuspendDomain('example.com'), + 'v-unsuspend-dns-domain', + ['arg1' => 'alice', 'arg2' => 'example.com'] + ]; + yield 'Dns::changeDomainIp' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeDomainIp('example.com', '192.0.2.20', true), + 'v-change-dns-domain-ip', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '192.0.2.20', 'arg4' => 'yes'] + ]; + yield 'Dns::changeDomainTtl' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeDomainTtl('example.com', 3600, true), + 'v-change-dns-domain-ttl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '3600', 'arg4' => 'yes'] + ]; + yield 'Dns::changeDomainExpiration' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeDomainExpiration('example.com', '2027-10-18'), + 'v-change-dns-domain-exp', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '2027-10-18'] + ]; + yield 'Dns::changeDomainSoa' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeDomainSoa('example.com', 'ns1.example.org', true), + 'v-change-dns-domain-soa', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'ns1.example.org', 'arg4' => 'yes'] + ]; + yield 'Dns::changeDomainTemplate' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeDomainTemplate('example.com', 'gmail', true), + 'v-change-dns-domain-tpl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'gmail', 'arg4' => 'yes'] + ]; + yield 'Dns::changeDomainDnssec' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeDomainDnssec('example.com', true), + 'v-change-dns-domain-dnssec', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Dns::addRecord' => [ + fn (Client $c) => $c->getModuleDns('alice')->addRecord('example.com', '@', 'MX', 'mail.example.com', 20, 30, true, 300), + 'v-add-dns-record', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '@', 'arg4' => 'MX', 'arg5' => 'mail.example.com', 'arg6' => '20', 'arg7' => '30', 'arg8' => 'yes', 'arg9' => '300'] + ]; + yield 'Dns::changeRecord' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeRecord('example.com', 30, '@', 'MX', 'mail.example.com', 20, true, 300), + 'v-change-dns-record', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '30', 'arg4' => '@', 'arg5' => 'MX', 'arg6' => 'mail.example.com', 'arg7' => '20', 'arg8' => 'yes', 'arg9' => '300'] + ]; + yield 'Dns::changeRecordId' => [ + fn (Client $c) => $c->getModuleDns('alice')->changeRecordId('example.com', 30, 31, true), + 'v-change-dns-record-id', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '30', 'arg4' => '31', 'arg5' => 'yes'] + ]; + yield 'Dns::suspendRecord' => [ + fn (Client $c) => $c->getModuleDns('alice')->suspendRecord('example.com', 30), + 'v-suspend-dns-record', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '30'] + ]; + yield 'Dns::unsuspendRecord' => [ + fn (Client $c) => $c->getModuleDns('alice')->unsuspendRecord('example.com', 30), + 'v-unsuspend-dns-record', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '30'] + ]; + yield 'Dns::deleteRecord' => [ + fn (Client $c) => $c->getModuleDns('alice')->deleteRecord('example.com', 30, true), + 'v-delete-dns-record', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '30', 'arg4' => 'yes'] + ]; + yield 'Mails::addAccount' => [ fn (Client $c) => $c->getModuleMail('alice')->addAccount('example.com', 'info', 'secret'), 'v-add-mail-account', @@ -165,6 +300,46 @@ public static function processProvider(): iterable { 'v-delete-mail-domain', ['arg1' => 'alice', 'arg2' => 'example.com'] ]; + yield 'Mails::addAccountAlias' => [ + fn (Client $c) => $c->getModuleMail('alice')->addAccountAlias('example.com', 'info', 'sales'), + 'v-add-mail-account-alias', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'sales'] + ]; + yield 'Mails::deleteAccountAlias' => [ + fn (Client $c) => $c->getModuleMail('alice')->deleteAccountAlias('example.com', 'info', 'sales'), + 'v-delete-mail-account-alias', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'sales'] + ]; + yield 'Mails::addAccountForward' => [ + fn (Client $c) => $c->getModuleMail('alice')->addAccountForward('example.com', 'info', 'bob@example.org'), + 'v-add-mail-account-forward', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'bob@example.org'] + ]; + yield 'Mails::deleteAccountForward' => [ + fn (Client $c) => $c->getModuleMail('alice')->deleteAccountForward('example.com', 'info', 'bob@example.org'), + 'v-delete-mail-account-forward', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'bob@example.org'] + ]; + yield 'Mails::addAccountForwardOnly' => [ + fn (Client $c) => $c->getModuleMail('alice')->addAccountForwardOnly('example.com', 'info'), + 'v-add-mail-account-fwd-only', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info'] + ]; + yield 'Mails::deleteAccountForwardOnly' => [ + fn (Client $c) => $c->getModuleMail('alice')->deleteAccountForwardOnly('example.com', 'info'), + 'v-delete-mail-account-fwd-only', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info'] + ]; + yield 'Mails::addAccountAutoreply' => [ + fn (Client $c) => $c->getModuleMail('alice')->addAccountAutoreply('example.com', 'info', 'Away until Monday.'), + 'v-add-mail-account-autoreply', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'Away until Monday.'] + ]; + yield 'Mails::deleteAccountAutoreply' => [ + fn (Client $c) => $c->getModuleMail('alice')->deleteAccountAutoreply('example.com', 'info'), + 'v-delete-mail-account-autoreply', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info'] + ]; yield 'Webs::addDomain' => [ fn (Client $c) => $c->getModuleWeb('alice')->addDomain('example.com', '10.0.0.1', 'www.example.com', 'jpg,png', true), @@ -216,6 +391,81 @@ public static function processProvider(): iterable { 'v-delete-web-domain-ftp', ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'ftpuser'] ]; + yield 'Webs::addDomainAlias' => [ + fn (Client $c) => $c->getModuleWeb('alice')->addDomainAlias('example.com', 'www.example.com,shop.example.com', true), + 'v-add-web-domain-alias', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'www.example.com,shop.example.com', 'arg4' => 'yes'] + ]; + yield 'Webs::deleteDomainAlias' => [ + fn (Client $c) => $c->getModuleWeb('alice')->deleteDomainAlias('example.com', 'www.example.com', false), + 'v-delete-web-domain-alias', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'www.example.com', 'arg4' => 'no'] + ]; + yield 'Webs::addDomainSsl' => [ + fn (Client $c) => $c->getModuleWeb('alice')->addDomainSsl('example.com', '/tmp/ssl', 'single', true), + 'v-add-web-domain-ssl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '/tmp/ssl', 'arg4' => 'single', 'arg5' => 'yes'] + ]; + yield 'Webs::changeDomainSslCert' => [ + fn (Client $c) => $c->getModuleWeb('alice')->changeDomainSslCert('example.com', '/tmp/ssl', true), + 'v-change-web-domain-sslcert', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => '/tmp/ssl', 'arg4' => 'yes'] + ]; + yield 'Webs::changeDomainSslHome' => [ + fn (Client $c) => $c->getModuleWeb('alice')->changeDomainSslHome('example.com', 'single', true), + 'v-change-web-domain-sslhome', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'single', 'arg4' => 'yes'] + ]; + yield 'Webs::deleteDomainSsl' => [ + fn (Client $c) => $c->getModuleWeb('alice')->deleteDomainSsl('example.com', true), + 'v-delete-web-domain-ssl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Webs::addDomainSslForce' => [ + fn (Client $c) => $c->getModuleWeb('alice')->addDomainSslForce('example.com', true), + 'v-add-web-domain-ssl-force', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Webs::deleteDomainSslForce' => [ + fn (Client $c) => $c->getModuleWeb('alice')->deleteDomainSslForce('example.com', true), + 'v-delete-web-domain-ssl-force', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Webs::addDomainSslHsts' => [ + fn (Client $c) => $c->getModuleWeb('alice')->addDomainSslHsts('example.com', true), + 'v-add-web-domain-ssl-hsts', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Webs::deleteDomainSslHsts' => [ + fn (Client $c) => $c->getModuleWeb('alice')->deleteDomainSslHsts('example.com', true), + 'v-delete-web-domain-ssl-hsts', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; + yield 'Webs::changeDomainTemplate' => [ + fn (Client $c) => $c->getModuleWeb('alice')->changeDomainTemplate('example.com', 'hosting', true), + 'v-change-web-domain-tpl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'hosting', 'arg4' => 'yes'] + ]; + yield 'Webs::changeDomainBackendTemplate' => [ + fn (Client $c) => $c->getModuleWeb('alice')->changeDomainBackendTemplate('example.com', 'PHP-8_3', true), + 'v-change-web-domain-backend-tpl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'PHP-8_3', 'arg4' => 'yes'] + ]; + yield 'Webs::changeDomainProxyTemplate' => [ + fn (Client $c) => $c->getModuleWeb('alice')->changeDomainProxyTemplate('example.com', 'caching', 'jpg,png', true), + 'v-change-web-domain-proxy-tpl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'caching', 'arg4' => 'jpg,png', 'arg5' => 'yes'] + ]; + yield 'Webs::addDomainProxy' => [ + fn (Client $c) => $c->getModuleWeb('alice')->addDomainProxy('example.com', 'caching', 'jpg,png', true), + 'v-add-web-domain-proxy', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'caching', 'arg4' => 'jpg,png', 'arg5' => 'yes'] + ]; + yield 'Webs::deleteDomainProxy' => [ + fn (Client $c) => $c->getModuleWeb('alice')->deleteDomainProxy('example.com', true), + 'v-delete-web-domain-proxy', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'yes'] + ]; } /** @@ -236,6 +486,25 @@ public static function listProvider(): iterable { ['arg1' => 'bob', 'arg2' => 'json'], true ]; + yield 'Users::stats' => [ + fn (Client $c) => $c->getModuleUser()->stats('bob'), + 'v-list-user-stats', + ['arg1' => 'bob', 'arg2' => 'json'], + false + ]; + + yield 'Packages::list' => [ + fn (Client $c) => $c->getModulePackage()->list(), + 'v-list-user-packages', + ['arg1' => 'json'], + false + ]; + yield 'Packages::detail' => [ + fn (Client $c) => $c->getModulePackage()->detail('gold'), + 'v-list-user-package', + ['arg1' => 'gold', 'arg2' => 'json'], + true + ]; yield 'Backups::listBackups' => [ fn (Client $c) => $c->getModuleBackup('alice')->listBackups(), @@ -269,6 +538,56 @@ public static function listProvider(): iterable { false ]; + yield 'Crons::listJobs' => [ + fn (Client $c) => $c->getModuleCron('alice')->listJobs(), + 'v-list-cron-jobs', + ['arg1' => 'alice', 'arg2' => 'json'], + false + ]; + yield 'Crons::listJob' => [ + fn (Client $c) => $c->getModuleCron('alice')->listJob(12), + 'v-list-cron-job', + ['arg1' => 'alice', 'arg2' => '12', 'arg3' => 'json'], + true + ]; + + yield 'Dns::listDomains' => [ + fn (Client $c) => $c->getModuleDns('alice')->listDomains(), + 'v-list-dns-domains', + ['arg1' => 'alice', 'arg2' => 'json'], + false + ]; + yield 'Dns::listDomain' => [ + fn (Client $c) => $c->getModuleDns('alice')->listDomain('example.com'), + 'v-list-dns-domain', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'json'], + true + ]; + yield 'Dns::listDnssecPublicKey' => [ + fn (Client $c) => $c->getModuleDns('alice')->listDnssecPublicKey('example.com'), + 'v-list-dnssec-public-key', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'json'], + true + ]; + yield 'Dns::listRecords' => [ + fn (Client $c) => $c->getModuleDns('alice')->listRecords('example.com'), + 'v-list-dns-records', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'json'], + false + ]; + yield 'Dns::listTemplates' => [ + fn (Client $c) => $c->getModuleDns('alice')->listTemplates(), + 'v-list-dns-templates', + ['arg1' => 'json'], + false + ]; + yield 'Dns::listTemplate' => [ + fn (Client $c) => $c->getModuleDns('alice')->listTemplate('gmail'), + 'v-list-dns-template', + ['arg1' => 'gmail', 'arg2' => 'json'], + false + ]; + yield 'Mails::listAccounts' => [ fn (Client $c) => $c->getModuleMail('alice')->listAccounts('example.com'), 'v-list-mail-accounts', @@ -293,6 +612,24 @@ public static function listProvider(): iterable { ['arg1' => 'alice', 'arg2' => 'json'], false ]; + yield 'Mails::listDomain' => [ + fn (Client $c) => $c->getModuleMail('alice')->listDomain('example.com'), + 'v-list-mail-domain', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'json'], + true + ]; + yield 'Mails::listAccount' => [ + fn (Client $c) => $c->getModuleMail('alice')->listAccount('example.com', 'info'), + 'v-list-mail-account', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'json'], + true + ]; + yield 'Mails::listAccountAutoreply' => [ + fn (Client $c) => $c->getModuleMail('alice')->listAccountAutoreply('example.com', 'info'), + 'v-list-mail-account-autoreply', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'info', 'arg4' => 'json'], + true + ]; yield 'Webs::listDomains' => [ fn (Client $c) => $c->getModuleWeb('alice')->listDomains(), @@ -300,6 +637,36 @@ public static function listProvider(): iterable { ['arg1' => 'alice', 'arg2' => 'json'], false ]; + yield 'Webs::listDomain' => [ + fn (Client $c) => $c->getModuleWeb('alice')->listDomain('example.com'), + 'v-list-web-domain', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'json'], + true + ]; + yield 'Webs::listDomainSsl' => [ + fn (Client $c) => $c->getModuleWeb('alice')->listDomainSsl('example.com'), + 'v-list-web-domain-ssl', + ['arg1' => 'alice', 'arg2' => 'example.com', 'arg3' => 'json'], + true + ]; + yield 'Webs::listTemplates' => [ + fn (Client $c) => $c->getModuleWeb('alice')->listTemplates(), + 'v-list-web-templates', + ['arg1' => 'json'], + false + ]; + yield 'Webs::listBackendTemplates' => [ + fn (Client $c) => $c->getModuleWeb('alice')->listBackendTemplates(), + 'v-list-web-templates-backend', + ['arg1' => 'json'], + false + ]; + yield 'Webs::listProxyTemplates' => [ + fn (Client $c) => $c->getModuleWeb('alice')->listProxyTemplates(), + 'v-list-web-templates-proxy', + ['arg1' => 'json'], + false + ]; } /** @@ -332,7 +699,7 @@ public function testListMethod(\Closure $call, string $cmd, array $args, bool $d */ public function testEveryModuleMethodIsCovered(): void { $methods = []; - foreach ([Users::class, Backups::class, Databases::class, Mails::class, Webs::class] as $module) { + foreach ([Users::class, Backups::class, Databases::class, Mails::class, Webs::class, Packages::class, Crons::class, Dns::class] as $module) { $reflection = new \ReflectionClass($module); foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { if ($method->getDeclaringClass()->getName() === $module && !$method->isConstructor()) { diff --git a/tests/Module/ModuleTest.php b/tests/Module/ModuleTest.php index 250f22c..8ca3f9e 100644 --- a/tests/Module/ModuleTest.php +++ b/tests/Module/ModuleTest.php @@ -6,7 +6,10 @@ use neto737\HestiaCP\Client; use neto737\HestiaCP\Module\Backups; +use neto737\HestiaCP\Module\Crons; use neto737\HestiaCP\Module\Databases; +use neto737\HestiaCP\Module\Dns; +use neto737\HestiaCP\Module\Packages; use neto737\HestiaCP\Module\Users; use neto737\HestiaCP\Module\Webs; use neto737\HestiaCP\Module\Mails; @@ -42,15 +45,33 @@ public function testDatabases(): void { $this->assertSame(Databases::class, get_class($client->getModuleDatabase('test'))); } + public function testModulePackage(): void { + $client = $this->createClient(); + $this->assertSame(Packages::class, get_class($client->getModulePackage())); + } + + public function testModuleCron(): void { + $client = $this->createClient(); + $this->assertSame(Crons::class, get_class($client->getModuleCron('test'))); + } + + public function testModuleDns(): void { + $client = $this->createClient(); + $this->assertSame(Dns::class, get_class($client->getModuleDns('test'))); + } + public function testModuleCachedPerUser(): void { $client = $this->createClient(); $this->assertSame($client->getModuleUser(), $client->getModuleUser()); + $this->assertSame($client->getModulePackage(), $client->getModulePackage()); $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')); + $this->assertNotSame($client->getModuleCron('alice'), $client->getModuleCron('bob')); + $this->assertNotSame($client->getModuleDns('alice'), $client->getModuleDns('bob')); } }