Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`, `process()` returns `T`), and `Client::send()` returns the command's result type. `ProcessCommand` is an `ICommand<bool>` and `ListCommand` an `ICommand<array<mixed>>`. Custom commands that extend `Command` directly can declare theirs with `@extends Command<...>`. Docblocks only, nothing changes at runtime.

Expand Down Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
60 changes: 36 additions & 24 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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:
Expand All @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions docs/cron.md
Original file line number Diff line number Diff line change
@@ -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)).
13 changes: 13 additions & 0 deletions docs/databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading