fix: align directadmin vacation api payload
This commit is contained in:
@@ -5,12 +5,15 @@ class DirectAdminVacationApi
|
|||||||
{
|
{
|
||||||
/** @var callable|null */
|
/** @var callable|null */
|
||||||
private $request;
|
private $request;
|
||||||
|
/** @var callable|null */
|
||||||
|
private $runner;
|
||||||
private string $daBinary = '/usr/local/directadmin/directadmin';
|
private string $daBinary = '/usr/local/directadmin/directadmin';
|
||||||
|
|
||||||
public function __construct(string $daBinary = '/usr/local/directadmin/directadmin', ?callable $request = null)
|
public function __construct(string $daBinary = '/usr/local/directadmin/directadmin', ?callable $request = null, ?callable $runner = null)
|
||||||
{
|
{
|
||||||
$this->daBinary = $daBinary;
|
$this->daBinary = $daBinary;
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
|
$this->runner = $runner;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function directAdminBinary(): string
|
public function directAdminBinary(): string
|
||||||
@@ -30,17 +33,15 @@ class DirectAdminVacationApi
|
|||||||
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
||||||
[$endYear, $endMonth, $endDay] = explode('-', $endDate);
|
[$endYear, $endMonth, $endDay] = explode('-', $endDate);
|
||||||
$subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '');
|
$subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '');
|
||||||
$replyFrequency = !empty($rule['reply_every_message']) ? '0' : (string)max(0, (int)($rule['repeat_minutes'] ?? 1440));
|
$payload = [
|
||||||
return [
|
|
||||||
'action' => $exists ? 'modify' : 'create',
|
'action' => $exists ? 'modify' : 'create',
|
||||||
'domain' => $domain,
|
'domain' => $domain,
|
||||||
'user' => $local,
|
'user' => $local,
|
||||||
'email' => $email,
|
|
||||||
'text' => (string)($rule['body'] ?? ''),
|
'text' => (string)($rule['body'] ?? ''),
|
||||||
'subject_prefix' => $subjectPrefix,
|
|
||||||
'subject' => $subjectPrefix,
|
'subject' => $subjectPrefix,
|
||||||
'reply_frequency' => $replyFrequency,
|
'reply_encoding' => 'UTF-8',
|
||||||
'reply_once_time' => $replyFrequency,
|
'reply_content_type' => 'text/plain',
|
||||||
|
'reply_once_time' => $this->replyOnceTime($rule),
|
||||||
'starttime' => $startTime,
|
'starttime' => $startTime,
|
||||||
'startyear' => $startYear,
|
'startyear' => $startYear,
|
||||||
'startmonth' => $startMonth,
|
'startmonth' => $startMonth,
|
||||||
@@ -49,8 +50,11 @@ class DirectAdminVacationApi
|
|||||||
'endyear' => $endYear,
|
'endyear' => $endYear,
|
||||||
'endmonth' => $endMonth,
|
'endmonth' => $endMonth,
|
||||||
'endday' => $endDay,
|
'endday' => $endDay,
|
||||||
'create' => $exists ? '' : 'Create',
|
|
||||||
];
|
];
|
||||||
|
if (!$exists) {
|
||||||
|
$payload['create'] = 'Create';
|
||||||
|
}
|
||||||
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed> $rule */
|
/** @param array<string,mixed> $rule */
|
||||||
@@ -127,13 +131,92 @@ class DirectAdminVacationApi
|
|||||||
}
|
}
|
||||||
$baseUrl = $this->apiUrl($owner);
|
$baseUrl = $this->apiUrl($owner);
|
||||||
$url = rtrim($baseUrl, '/') . '/' . $endpoint;
|
$url = rtrim($baseUrl, '/') . '/' . $endpoint;
|
||||||
$args = ['curl', '-fsS', '-X', 'POST'];
|
$args = ['curl', '-sS', '-X', 'POST', '-w', "\n%{http_code}"];
|
||||||
foreach ($payload as $key => $value) {
|
foreach ($payload as $key => $value) {
|
||||||
$args[] = '--data-urlencode';
|
$args[] = '--data-urlencode';
|
||||||
$args[] = $key . '=' . $value;
|
$args[] = $key . '=' . $value;
|
||||||
}
|
}
|
||||||
$args[] = $url;
|
$args[] = $url;
|
||||||
return $this->run($args, '');
|
return $this->parseHttpResponse($this->run($args, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $rule */
|
||||||
|
private function replyOnceTime(array $rule): string
|
||||||
|
{
|
||||||
|
if (!empty($rule['reply_every_message'])) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return match ((int)($rule['repeat_minutes'] ?? 1440)) {
|
||||||
|
30 => '30m',
|
||||||
|
60 => '1h',
|
||||||
|
120 => '2h',
|
||||||
|
360 => '6h',
|
||||||
|
720 => '12h',
|
||||||
|
1440 => '1d',
|
||||||
|
2880 => '2d',
|
||||||
|
10080 => '1w',
|
||||||
|
default => '1d',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseHttpResponse(string $output): string
|
||||||
|
{
|
||||||
|
if (!preg_match("/\n([0-9]{3})$/", $output, $m, PREG_OFFSET_CAPTURE)) {
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
$code = (int)$m[1][0];
|
||||||
|
$body = substr($output, 0, (int)$m[0][1]);
|
||||||
|
if ($code < 200 || $code >= 400) {
|
||||||
|
throw new RuntimeException('DirectAdmin API HTTP ' . $code . ': ' . $this->compactBody($body));
|
||||||
|
}
|
||||||
|
$this->throwIfApiError($body);
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function throwIfApiError(string $body): void
|
||||||
|
{
|
||||||
|
$trimmed = trim($body);
|
||||||
|
if ($trimmed === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_decode($trimmed, true);
|
||||||
|
if (is_array($json) && isset($json['error']) && $this->truthyError($json['error'])) {
|
||||||
|
throw new RuntimeException('DirectAdmin API error: ' . $this->compactBody(implode(' ', array_filter([
|
||||||
|
(string)($json['text'] ?? ''),
|
||||||
|
(string)($json['details'] ?? ''),
|
||||||
|
(string)($json['result'] ?? ''),
|
||||||
|
(string)($json['error'] ?? ''),
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = [];
|
||||||
|
parse_str($trimmed, $fields);
|
||||||
|
if (isset($fields['error']) && $this->truthyError($fields['error'])) {
|
||||||
|
throw new RuntimeException('DirectAdmin API error: ' . $this->compactBody(implode(' ', array_filter([
|
||||||
|
is_scalar($fields['text'] ?? null) ? (string)$fields['text'] : '',
|
||||||
|
is_scalar($fields['details'] ?? null) ? (string)$fields['details'] : '',
|
||||||
|
is_scalar($fields['result'] ?? null) ? (string)$fields['result'] : '',
|
||||||
|
is_scalar($fields['error'] ?? null) ? (string)$fields['error'] : '',
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function truthyError(mixed $value): bool
|
||||||
|
{
|
||||||
|
return in_array(strtolower(trim((string)$value)), ['1', 'yes', 'true', 'error'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compactBody(string $body): string
|
||||||
|
{
|
||||||
|
$body = trim(preg_replace('/[[:cntrl:]]+/', ' ', $body) ?? $body);
|
||||||
|
if ($body === '') {
|
||||||
|
return 'empty response body';
|
||||||
|
}
|
||||||
|
if (strlen($body) > 1200) {
|
||||||
|
return substr($body, 0, 1200) . '...';
|
||||||
|
}
|
||||||
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function apiUrl(string $owner): string
|
private function apiUrl(string $owner): string
|
||||||
@@ -164,6 +247,10 @@ class DirectAdminVacationApi
|
|||||||
/** @param string[] $args */
|
/** @param string[] $args */
|
||||||
private function run(array $args, string $stdin): string
|
private function run(array $args, string $stdin): string
|
||||||
{
|
{
|
||||||
|
if ($this->runner !== null) {
|
||||||
|
$result = call_user_func($this->runner, $args, $stdin);
|
||||||
|
return (string)$result;
|
||||||
|
}
|
||||||
if (($args[0] ?? '') !== '' && str_contains($args[0], '/') && !is_executable($args[0])) {
|
if (($args[0] ?? '') !== '' && str_contains($args[0], '/') && !is_executable($args[0])) {
|
||||||
throw new RuntimeException('DirectAdmin API command is not executable: ' . $args[0]);
|
throw new RuntimeException('DirectAdmin API command is not executable: ' . $args[0]);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name=global-autoresponder
|
|||||||
id=global-autoresponder
|
id=global-autoresponder
|
||||||
type=user
|
type=user
|
||||||
author=HITME.PL
|
author=HITME.PL
|
||||||
version=1.1.2
|
version=1.1.3
|
||||||
active=no
|
active=no
|
||||||
installed=no
|
installed=no
|
||||||
user_run_as=root
|
user_run_as=root
|
||||||
|
|||||||
@@ -22,12 +22,70 @@ test('directadmin vacation api builds scoped payload for one mailbox', function
|
|||||||
assert_same('info', $payload['user']);
|
assert_same('info', $payload['user']);
|
||||||
assert_same('example.com', $payload['domain']);
|
assert_same('example.com', $payload['domain']);
|
||||||
assert_same('Body', $payload['text']);
|
assert_same('Body', $payload['text']);
|
||||||
assert_same('Urlop', $payload['subject_prefix']);
|
assert_same('Urlop', $payload['subject']);
|
||||||
assert_same('1440', $payload['reply_frequency']);
|
assert_same('1d', $payload['reply_once_time']);
|
||||||
|
assert_false(isset($payload['email']));
|
||||||
|
assert_false(isset($payload['subject_prefix']));
|
||||||
|
assert_false(isset($payload['reply_frequency']));
|
||||||
assert_same('morning', $payload['starttime']);
|
assert_same('morning', $payload['starttime']);
|
||||||
assert_same('evening', $payload['endtime']);
|
assert_same('evening', $payload['endtime']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('directadmin vacation api reports http error response bodies', function (): void {
|
||||||
|
$runner = static function (array $args, string $stdin): string {
|
||||||
|
if ($args[0] === '/bin/echo') {
|
||||||
|
return 'https://directadmin.invalid/CMD_LOGIN_KEY';
|
||||||
|
}
|
||||||
|
assert_same('curl', $args[0]);
|
||||||
|
return "error=1&text=Unable to set vacation&details=Invalid repeat value\n500";
|
||||||
|
};
|
||||||
|
|
||||||
|
$api = new DirectAdminVacationApi('/bin/echo', null, $runner);
|
||||||
|
try {
|
||||||
|
$api->saveVacation('alice', 'info@example.com', [
|
||||||
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
|
'body' => 'Body',
|
||||||
|
'start_value' => '2026-06-10|morning',
|
||||||
|
'end_value' => '2026-06-24|evening',
|
||||||
|
], false);
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assert_contains('DirectAdmin API HTTP 500', $e->getMessage());
|
||||||
|
assert_contains('Invalid repeat value', $e->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException('Expected HTTP error to fail');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('directadmin vacation api reports legacy api error payloads', function (): void {
|
||||||
|
$runner = static function (array $args, string $stdin): string {
|
||||||
|
if ($args[0] === '/bin/echo') {
|
||||||
|
return 'https://directadmin.invalid/CMD_LOGIN_KEY';
|
||||||
|
}
|
||||||
|
return "error=1&text=Unable%20to%20set%20vacation&details=Bad%20mailbox\n200";
|
||||||
|
};
|
||||||
|
|
||||||
|
$api = new DirectAdminVacationApi('/bin/echo', null, $runner);
|
||||||
|
try {
|
||||||
|
$api->saveVacation('alice', 'info@example.com', [
|
||||||
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
|
'body' => 'Body',
|
||||||
|
'start_value' => '2026-06-10|morning',
|
||||||
|
'end_value' => '2026-06-24|evening',
|
||||||
|
], false);
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assert_contains('DirectAdmin API error', $e->getMessage());
|
||||||
|
assert_contains('Bad mailbox', $e->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException('Expected legacy API error to fail');
|
||||||
|
});
|
||||||
|
|
||||||
test('directadmin vacation api detects existing native vacation by mailbox', function (): void {
|
test('directadmin vacation api detects existing native vacation by mailbox', function (): void {
|
||||||
$api = new DirectAdminVacationApi('/usr/local/directadmin/directadmin', static function (string $owner, string $endpoint, array $payload): string {
|
$api = new DirectAdminVacationApi('/usr/local/directadmin/directadmin', static function (string $owner, string $endpoint, array $payload): string {
|
||||||
assert_same('alice', $owner);
|
assert_same('alice', $owner);
|
||||||
|
|||||||
Reference in New Issue
Block a user