feat: derive autoresponder schedule from backend
This commit is contained in:
@@ -12,6 +12,7 @@ define('PLUGIN_EXEC_DIR', __DIR__);
|
||||
|
||||
foreach ([
|
||||
'Settings.php',
|
||||
'BackendRuntimeConfig.php',
|
||||
'Http.php',
|
||||
'DirectAdminUser.php',
|
||||
'Lang.php',
|
||||
@@ -30,6 +31,7 @@ require_once PLUGIN_EXEC_DIR . '/handlers/_form_helpers.php';
|
||||
|
||||
try {
|
||||
$settings = Settings::load(Settings::EXTERNAL_SETTINGS);
|
||||
BackendRuntimeConfig::sync($settings, PLUGIN_ROOT . '/scripts/backend.sh', Settings::CONFIG_DIR . '/backend-state.json');
|
||||
$user = DirectAdminUser::fromEnvironment($settings);
|
||||
$lang = Lang::load($user->language(), $settings);
|
||||
if (!$user->hasPluginAccess($settings)) {
|
||||
|
||||
@@ -16,14 +16,12 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
|
||||
$hiddenId = $isUpdate ? '<input type="hidden" name="id" value="' . AppContext::e($id) . '">' : '';
|
||||
|
||||
return '<form method="post" action="' . AppContext::e($action) . '">' . $csrf . $hiddenId .
|
||||
'<div class="grid grid-2"><div class="panel"><h3>Treść odpowiedzi</h3><div class="row"><div><label>' . AppContext::e($ctx->t('Subject')) . '</label><input type="text" name="subject" value="' . AppContext::e((string)$subject) . '"></div>' .
|
||||
'<div class="panel"><h3>' . AppContext::e($ctx->t('Response content')) . '</h3><div class="row"><div><label>' . AppContext::e($ctx->t('Subject')) . '</label><input type="text" name="subject" value="' . AppContext::e((string)$subject) . '"></div>' .
|
||||
'<div><label>' . AppContext::e($ctx->t('Status')) . '</label><label class="br-time-item"><input type="checkbox" name="enabled" value="1" ' . ($enabled ? 'checked' : '') . '> ' . AppContext::e($ctx->t('Enabled')) . '</label></div></div>' .
|
||||
'<label>' . AppContext::e($ctx->t('Message body')) . '</label><textarea name="body">' . AppContext::e((string)$body) . '</textarea></div>' .
|
||||
'<div class="panel"><h3>Podsumowanie</h3><div class="summary-card"><div class="kv"><strong>Zakres</strong><span>' . AppContext::e($ctx->t('All valid POP/IMAP mailboxes in this account')) . '</span></div>' .
|
||||
'<div class="kv"><strong>Start</strong><span>' . AppContext::e($startValue) . '</span></div><div class="kv"><strong>Koniec</strong><span>' . AppContext::e($endValue) . '</span></div></div>' .
|
||||
'<div class="actions"><button class="primary" type="submit">' . AppContext::e($ctx->t($isUpdate ? 'Save changes' : 'Save autoresponder')) . '</button><a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">' . AppContext::e($ctx->t('Cancel')) . '</a></div></div></div>' .
|
||||
render_calendar_picker('start_value', 'Data rozpoczęcia', $startValue) .
|
||||
render_calendar_picker('end_value', 'Data zakończenia', $endValue) .
|
||||
render_calendar_picker($ctx, 'start_value', $ctx->t('Start date'), $startValue) .
|
||||
render_calendar_picker($ctx, 'end_value', $ctx->t('End date'), $endValue) .
|
||||
'<div class="panel"><div class="actions"><button class="primary" type="submit">' . AppContext::e($ctx->t($isUpdate ? 'Save changes' : 'Save autoresponder')) . '</button><a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">' . AppContext::e($ctx->t('Cancel')) . '</a></div></div>' .
|
||||
'</form>';
|
||||
}
|
||||
|
||||
@@ -32,18 +30,28 @@ function normalize_rule_post(array $post): array
|
||||
{
|
||||
foreach (['start_value', 'end_value'] as $key) {
|
||||
$date = trim((string)($post[$key . '_date'] ?? ''));
|
||||
$hour = trim((string)($post[$key . '_hour'] ?? ''));
|
||||
if ($date !== '' && preg_match('/^[0-9]{2}$/', $hour)) {
|
||||
$post[$key] = $date . 'T' . $hour . ':00';
|
||||
$time = trim((string)($post[$key . '_time'] ?? ''));
|
||||
$period = trim((string)($post[$key . '_period'] ?? ''));
|
||||
if ($date !== '' && preg_match('/^[0-9]{2}:[0-9]{2}$/', $time)) {
|
||||
$post[$key] = $date . 'T' . $time;
|
||||
} elseif ($date !== '' && preg_match('/^(morning|afternoon|evening)$/', $period)) {
|
||||
$post[$key] = $date . '|' . $period;
|
||||
}
|
||||
}
|
||||
return $post;
|
||||
}
|
||||
|
||||
function render_calendar_picker(string $name, string $title, string $value): string
|
||||
function render_calendar_picker(AppContext $ctx, string $name, string $title, string $value): string
|
||||
{
|
||||
$mode = $ctx->settings->scheduleMode();
|
||||
$selectedDate = substr($value, 0, 10);
|
||||
$selectedHour = substr($value, 11, 2) ?: '09';
|
||||
$selectedTime = '09:00';
|
||||
$selectedPeriod = 'morning';
|
||||
if ($mode === 'exact_hour' && preg_match('/T([0-9]{2}:[0-9]{2})$/', $value, $m)) {
|
||||
$selectedTime = $m[1];
|
||||
} elseif ($mode === 'directadmin_period' && preg_match('/\|(morning|afternoon|evening)$/', $value, $m)) {
|
||||
$selectedPeriod = $m[1];
|
||||
}
|
||||
$days = '';
|
||||
for ($week = 0, $day = 1; $week < 4; $week++) {
|
||||
$days .= '<tr>';
|
||||
@@ -54,14 +62,19 @@ function render_calendar_picker(string $name, string $title, string $value): str
|
||||
}
|
||||
$days .= '</tr>';
|
||||
}
|
||||
$hours = '';
|
||||
foreach (['08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18'] as $hour) {
|
||||
$hours .= '<label class="br-time-item"><input type="radio" name="' . AppContext::e($name . '_hour') . '" value="' . $hour . '" ' . ($hour === $selectedHour ? 'checked' : '') . '> ' . $hour . ':00</label>';
|
||||
if ($mode === 'exact_hour') {
|
||||
$timeControl = '<label>' . AppContext::e($ctx->t('Time')) . '</label><input type="time" name="' . AppContext::e($name . '_time') . '" value="' . AppContext::e($selectedTime) . '" step="60">';
|
||||
} else {
|
||||
$timeControl = '<label>' . AppContext::e($ctx->t('Time of day')) . '</label><select name="' . AppContext::e($name . '_period') . '">' .
|
||||
'<option value="morning"' . ($selectedPeriod === 'morning' ? ' selected' : '') . '>' . AppContext::e($ctx->t('Morning')) . '</option>' .
|
||||
'<option value="afternoon"' . ($selectedPeriod === 'afternoon' ? ' selected' : '') . '>' . AppContext::e($ctx->t('Afternoon')) . '</option>' .
|
||||
'<option value="evening"' . ($selectedPeriod === 'evening' ? ' selected' : '') . '>' . AppContext::e($ctx->t('Evening')) . '</option>' .
|
||||
'</select>';
|
||||
}
|
||||
return '<div class="panel"><h3>' . AppContext::e($title) . '</h3><div class="br-restore-layout"><div class="br-restore-calendar"><h4>' . AppContext::e($title) . '</h4><div class="br-month-nav"><button type="button" class="br-month-btn">‹</button><span class="br-month-label">Czerwiec 2026</span><button type="button" class="br-month-btn">›</button></div>' .
|
||||
'<table class="br-calendar-grid"><thead><tr><th>Pn</th><th>Wt</th><th>Śr</th><th>Cz</th><th>Pt</th><th>Sb</th><th>Nd</th></tr></thead><tbody>' . $days . '</tbody></table>' .
|
||||
'<div class="br-date-label" id="' . AppContext::e($name . '_label') . '">Wybrano: ' . AppContext::e($selectedDate) . '</div><input type="hidden" id="' . AppContext::e($name . '_date') . '" name="' . AppContext::e($name . '_date') . '" value="' . AppContext::e($selectedDate) . '"></div>' .
|
||||
'<div class="br-restore-calendar"><h4>Godzina</h4><div class="br-time-picker"><div class="br-time-title">Wybierz godzinę</div><div class="br-time-list">' . $hours . '</div></div></div></div>' .
|
||||
'<div class="br-restore-calendar"><h4>' . AppContext::e($ctx->t('Time')) . '</h4>' . $timeControl . '</div></div>' .
|
||||
'<input type="hidden" name="' . AppContext::e($name) . '" value="' . AppContext::e($value) . '"></div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ return static function (AppContext $ctx): void {
|
||||
$id = Http::isPost() ? $ctx->post('id') : $ctx->query('id');
|
||||
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
|
||||
if ($rule === null) {
|
||||
$ctx->render('Confirm deletion', '<div class="alert alert-err">Rule not found</div>');
|
||||
$ctx->render('Confirm deletion', '<div class="alert alert-err">' . AppContext::e($ctx->t('Rule not found')) . '</div>');
|
||||
return;
|
||||
}
|
||||
if (Http::isPost()) {
|
||||
@@ -20,7 +20,7 @@ return static function (AppContext $ctx): void {
|
||||
}
|
||||
}
|
||||
$body = '<div class="grid grid-2"><div class="panel"><h3>' . AppContext::e($ctx->t('Preview autoresponder')) . '</h3>' . render_preview_box($rule) . '</div>' .
|
||||
'<div class="panel"><h3>' . AppContext::e($ctx->t('Confirm deletion')) . '</h3><div class="alert alert-err">Usunięcie reguły wyłączy odpowiedź automatyczną utworzoną przez tę regułę dla objętych skrzynek.</div>' .
|
||||
'<div class="panel"><h3>' . AppContext::e($ctx->t('Confirm deletion')) . '</h3><div class="alert alert-err">' . AppContext::e($ctx->t('Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.')) . '</div>' .
|
||||
'<form method="post" action="' . AppContext::e($ctx->url('delete.html')) . '">' . $ctx->csrfField('delete:' . $id) . '<input type="hidden" name="id" value="' . AppContext::e($id) . '">' .
|
||||
'<div class="actions"><button class="danger-fill" type="submit">' . AppContext::e($ctx->t('Delete autoresponder')) . '</button><a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">' . AppContext::e($ctx->t('Do not delete')) . '</a></div></form></div></div>';
|
||||
$ctx->render('Confirm deletion', $body);
|
||||
|
||||
@@ -24,8 +24,8 @@ return static function (AppContext $ctx): void {
|
||||
$enabled = !empty($rule['enabled']);
|
||||
$rows .= '<tr><td><span class="badge ' . ($enabled ? 'ok' : 'off') . '">' . AppContext::e($ctx->t($enabled ? 'Enabled' : 'Disabled')) . '</span></td>';
|
||||
$rows .= '<td>' . AppContext::e((string)$rule['subject']) . '<br><span class="muted">' . date('Y-m-d H:i', (int)($rule['updated_at'] ?? time())) . '</span></td>';
|
||||
$rows .= '<td>' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts'])) . '<br><span class="muted">do ' . AppContext::e(date('Y-m-d H:i', (int)$rule['end_ts'])) . '</span></td>';
|
||||
$rows .= '<td>' . AppContext::e($rule['dynamic_scope'] ? 'Dynamiczny' : 'Snapshot') . '</td><td><span class="table-actions">';
|
||||
$rows .= '<td>' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts'])) . '<br><span class="muted">' . AppContext::e($ctx->t('To')) . ' ' . AppContext::e(date('Y-m-d H:i', (int)$rule['end_ts'])) . '</span></td>';
|
||||
$rows .= '<td>' . AppContext::e($ctx->t($rule['dynamic_scope'] ? 'Dynamic' : 'Snapshot')) . '</td><td><span class="table-actions">';
|
||||
$rows .= '<a class="btn" href="' . AppContext::e($ctx->url('update.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Edit')) . '</a>';
|
||||
$rows .= '<a class="btn" href="' . AppContext::e($ctx->url('preview.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Preview')) . '</a>';
|
||||
$rows .= '<form method="post" action="' . AppContext::e($ctx->url('toggle.html')) . '">' . $ctx->csrfField('toggle:' . $id) . '<input type="hidden" name="id" value="' . AppContext::e($id) . '"><button>' . AppContext::e($ctx->t($enabled ? 'Disable' : 'Enable')) . '</button></form>';
|
||||
@@ -35,6 +35,6 @@ return static function (AppContext $ctx): void {
|
||||
|
||||
$body = '<div class="panel"><div class="panel-head"><div><h3>' . AppContext::e($ctx->t('Global autoresponders')) . '</h3><p class="muted">' . AppContext::e($ctx->t('All valid POP/IMAP mailboxes in this account')) . '</p></div>' .
|
||||
'<a class="btn amber" href="' . AppContext::e($ctx->url('create.html')) . '">' . AppContext::e($ctx->t('Add autoresponder')) . '</a></div>' .
|
||||
'<table><thead><tr><th>Status</th><th>' . AppContext::e($ctx->t('Subject')) . '</th><th>Okres wysyłki</th><th>Zakres</th><th>Akcje</th></tr></thead><tbody>' . $rows . '</tbody></table></div>';
|
||||
'<table><thead><tr><th>' . AppContext::e($ctx->t('Status')) . '</th><th>' . AppContext::e($ctx->t('Subject')) . '</th><th>' . AppContext::e($ctx->t('Sending period')) . '</th><th>' . AppContext::e($ctx->t('Scope')) . '</th><th>' . AppContext::e($ctx->t('Actions')) . '</th></tr></thead><tbody>' . $rows . '</tbody></table></div>';
|
||||
$ctx->render('Global autoresponders', $body, $ok);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,12 @@ return static function (AppContext $ctx): void {
|
||||
$id = $ctx->query('id');
|
||||
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
|
||||
if ($rule === null) {
|
||||
$ctx->render('Preview autoresponder', '<div class="alert alert-err">Rule not found</div>');
|
||||
$ctx->render('Preview autoresponder', '<div class="alert alert-err">' . AppContext::e($ctx->t('Rule not found')) . '</div>');
|
||||
return;
|
||||
}
|
||||
$body = '<div class="grid grid-2"><div class="panel"><h3>' . AppContext::e($ctx->t('Preview autoresponder')) . '</h3>' . render_preview_box($rule) . '</div>' .
|
||||
'<div class="panel"><h3>Szczegóły reguły</h3><div class="summary-card"><div class="kv"><strong>Status</strong><span>' . AppContext::e($ctx->t(!empty($rule['enabled']) ? 'Enabled' : 'Disabled')) . '</span></div>' .
|
||||
'<div class="kv"><strong>Okres</strong><span>' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts']) . ' - ' . date('Y-m-d H:i', (int)$rule['end_ts'])) . '</span></div></div>' .
|
||||
'<div class="actions"><a class="btn" href="' . AppContext::e($ctx->url('update.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Edit')) . '</a><a class="btn danger" href="' . AppContext::e($ctx->url('delete.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Delete')) . '</a><a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Powrót</a></div></div></div>';
|
||||
'<div class="panel"><h3>' . AppContext::e($ctx->t('Rule details')) . '</h3><div class="summary-card"><div class="kv"><strong>' . AppContext::e($ctx->t('Status')) . '</strong><span>' . AppContext::e($ctx->t(!empty($rule['enabled']) ? 'Enabled' : 'Disabled')) . '</span></div>' .
|
||||
'<div class="kv"><strong>' . AppContext::e($ctx->t('Sending period')) . '</strong><span>' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts']) . ' - ' . date('Y-m-d H:i', (int)$rule['end_ts'])) . '</span></div></div>' .
|
||||
'<div class="actions"><a class="btn" href="' . AppContext::e($ctx->url('update.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Edit')) . '</a><a class="btn danger" href="' . AppContext::e($ctx->url('delete.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Delete')) . '</a><a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">' . AppContext::e($ctx->t('Back')) . '</a></div></div></div>';
|
||||
$ctx->render('Preview autoresponder', $body);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ return static function (AppContext $ctx): void {
|
||||
$ctx->requireCsrf('toggle:' . $id);
|
||||
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
|
||||
if ($rule === null) {
|
||||
throw new RuntimeException('Rule not found');
|
||||
throw new RuntimeException($ctx->t('Rule not found'));
|
||||
}
|
||||
$ctx->rules->update($ctx->daUser->username(), $id, ['enabled' => empty($rule['enabled'])]);
|
||||
Http::redirect($ctx->url('index.html', ['status' => 'toggled']));
|
||||
|
||||
@@ -5,7 +5,7 @@ return static function (AppContext $ctx): void {
|
||||
$id = Http::isPost() ? $ctx->post('id') : $ctx->query('id');
|
||||
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
|
||||
if ($rule === null) {
|
||||
$ctx->render('Edit autoresponder', '<div class="alert alert-err">Rule not found</div>');
|
||||
$ctx->render('Edit autoresponder', '<div class="alert alert-err">' . AppContext::e($ctx->t('Rule not found')) . '</div>');
|
||||
return;
|
||||
}
|
||||
$errors = [];
|
||||
|
||||
@@ -108,14 +108,16 @@ final class AppContext
|
||||
|
||||
private function scripts(): string
|
||||
{
|
||||
return <<<'JS'
|
||||
$selectedLabel = json_encode($this->t('Selected'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
|
||||
return <<<JS
|
||||
(function () {
|
||||
var selectedLabel = {$selectedLabel};
|
||||
document.querySelectorAll('[data-date-value]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var target = document.getElementById(btn.getAttribute('data-date-target'));
|
||||
var label = document.getElementById(btn.getAttribute('data-date-label'));
|
||||
if (target) { target.value = btn.getAttribute('data-date-value') || ''; }
|
||||
if (label) { label.textContent = 'Wybrano: ' + (btn.getAttribute('data-date-value') || ''); }
|
||||
if (label) { label.textContent = selectedLabel + ': ' + (btn.getAttribute('data-date-value') || ''); }
|
||||
btn.closest('.br-restore-calendar').querySelectorAll('.br-cal-day').forEach(function (d) { d.classList.remove('is-selected'); });
|
||||
btn.classList.add('is-selected');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class BackendRuntimeConfig
|
||||
{
|
||||
public static function sync(Settings $settings, string $backendScript, string $statePath = Settings::CONFIG_DIR . '/backend-state.json', string $errorLog = ''): void
|
||||
{
|
||||
$desired = $settings->backendMode();
|
||||
$current = self::currentBackend($statePath);
|
||||
if ($current === $desired) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_file($backendScript) || !is_executable($backendScript)) {
|
||||
self::log($errorLog, 'Backend script not executable: ' . $backendScript);
|
||||
return;
|
||||
}
|
||||
|
||||
$process = proc_open([$backendScript, $desired], [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
|
||||
if (!is_resource($process)) {
|
||||
self::log($errorLog, 'Could not start backend script.');
|
||||
return;
|
||||
}
|
||||
fclose($pipes[0]);
|
||||
stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$code = proc_close($process);
|
||||
if ($code !== 0) {
|
||||
self::log($errorLog, 'Backend script failed with code ' . $code . ': ' . trim($stderr));
|
||||
}
|
||||
}
|
||||
|
||||
private static function currentBackend(string $statePath): string
|
||||
{
|
||||
if (!is_file($statePath)) {
|
||||
return '';
|
||||
}
|
||||
$data = json_decode((string)file_get_contents($statePath), true);
|
||||
return is_array($data) ? (string)($data['active_backend'] ?? '') : '';
|
||||
}
|
||||
|
||||
private static function log(string $errorLog, string $message): void
|
||||
{
|
||||
if ($errorLog === '') {
|
||||
return;
|
||||
}
|
||||
@error_log('[' . date('c') . '] BACKEND_SYNC ' . $message . "\n", 3, $errorLog);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ final class RuleValidator
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($mode === 'exact_hour') {
|
||||
if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:00$/', $value)) {
|
||||
if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}$/', $value)) {
|
||||
throw new InvalidArgumentException('Nieprawidłowa data lub godzina.');
|
||||
}
|
||||
$dt = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $value, $tz);
|
||||
|
||||
@@ -51,7 +51,7 @@ final class Settings
|
||||
'DEFAULT_PLUGIN_LANGUAGE' => 'en',
|
||||
'DEFAULT_ENABLE' => 'true',
|
||||
'GLOBAL_AUTORESPONDER_TIMEZONE' => 'Europe/Warsaw',
|
||||
'GLOBAL_AUTORESPONDER_SCHEDULE_MODE' => 'exact_hour',
|
||||
'GLOBAL_AUTORESPONDER_BACKEND' => 'da',
|
||||
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE' => 'false',
|
||||
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES' => '1440',
|
||||
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE' => 'true',
|
||||
@@ -93,9 +93,14 @@ final class Settings
|
||||
return $this->getString('GLOBAL_AUTORESPONDER_TIMEZONE', 'Europe/Warsaw');
|
||||
}
|
||||
|
||||
public function backendMode(): string
|
||||
{
|
||||
return $this->getString('GLOBAL_AUTORESPONDER_BACKEND', 'da');
|
||||
}
|
||||
|
||||
public function scheduleMode(): string
|
||||
{
|
||||
return $this->getString('GLOBAL_AUTORESPONDER_SCHEDULE_MODE', 'exact_hour');
|
||||
return $this->backendMode() === 'da' ? 'directadmin_period' : 'exact_hour';
|
||||
}
|
||||
|
||||
public function replyEveryMessage(): bool
|
||||
@@ -148,8 +153,8 @@ final class Settings
|
||||
} catch (Throwable) {
|
||||
throw new InvalidArgumentException('Invalid timezone: ' . $this->timezone());
|
||||
}
|
||||
if (!in_array($this->scheduleMode(), ['exact_hour', 'directadmin_period'], true)) {
|
||||
throw new InvalidArgumentException('Invalid schedule mode: ' . $this->scheduleMode());
|
||||
if (!in_array($this->backendMode(), ['da', 'exim'], true)) {
|
||||
throw new InvalidArgumentException('Invalid backend mode: ' . $this->backendMode());
|
||||
}
|
||||
if ($this->repeatMinutes() < 0) {
|
||||
throw new InvalidArgumentException('Invalid repeat minutes');
|
||||
|
||||
+17
@@ -12,6 +12,7 @@ return [
|
||||
'Preview autoresponder' => 'Preview autoresponder',
|
||||
'Subject' => 'Subject',
|
||||
'Message body' => 'Message body',
|
||||
'Response content' => 'Response content',
|
||||
'Status' => 'Status',
|
||||
'Enabled' => 'Enabled',
|
||||
'Disabled' => 'Disabled',
|
||||
@@ -19,6 +20,11 @@ return [
|
||||
'End date' => 'End date',
|
||||
'Start hour' => 'Start hour',
|
||||
'End hour' => 'End hour',
|
||||
'Time' => 'Time',
|
||||
'Time of day' => 'Time of day',
|
||||
'Morning' => 'Morning',
|
||||
'Afternoon' => 'Afternoon',
|
||||
'Evening' => 'Evening',
|
||||
'Save autoresponder' => 'Save autoresponder',
|
||||
'Save changes' => 'Save changes',
|
||||
'Cancel' => 'Cancel',
|
||||
@@ -36,4 +42,15 @@ return [
|
||||
'Rule deleted.' => 'Rule deleted.',
|
||||
'Rule status changed.' => 'Rule status changed.',
|
||||
'All valid POP/IMAP mailboxes in this account' => 'All valid POP/IMAP mailboxes in this account',
|
||||
'Rule not found' => 'Rule not found',
|
||||
'Sending period' => 'Sending period',
|
||||
'Scope' => 'Scope',
|
||||
'Actions' => 'Actions',
|
||||
'Dynamic' => 'Dynamic',
|
||||
'Snapshot' => 'Snapshot',
|
||||
'To' => 'to',
|
||||
'Back' => 'Back',
|
||||
'Rule details' => 'Rule details',
|
||||
'Selected' => 'Selected',
|
||||
'Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.' => 'Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.',
|
||||
];
|
||||
|
||||
+17
@@ -12,6 +12,7 @@ return [
|
||||
'Preview autoresponder' => 'Podgląd autorespondera',
|
||||
'Subject' => 'Temat',
|
||||
'Message body' => 'Treść wiadomości',
|
||||
'Response content' => 'Treść odpowiedzi',
|
||||
'Status' => 'Status',
|
||||
'Enabled' => 'Włączony',
|
||||
'Disabled' => 'Wyłączony',
|
||||
@@ -19,6 +20,11 @@ return [
|
||||
'End date' => 'Data zakończenia',
|
||||
'Start hour' => 'Godzina rozpoczęcia',
|
||||
'End hour' => 'Godzina zakończenia',
|
||||
'Time' => 'Czas',
|
||||
'Time of day' => 'Pora dnia',
|
||||
'Morning' => 'Rano',
|
||||
'Afternoon' => 'Po południu',
|
||||
'Evening' => 'Wieczorem',
|
||||
'Save autoresponder' => 'Zapisz autoresponder',
|
||||
'Save changes' => 'Zapisz zmiany',
|
||||
'Cancel' => 'Anuluj',
|
||||
@@ -36,4 +42,15 @@ return [
|
||||
'Rule deleted.' => 'Reguła została usunięta.',
|
||||
'Rule status changed.' => 'Status reguły został zmieniony.',
|
||||
'All valid POP/IMAP mailboxes in this account' => 'Wszystkie prawidłowe skrzynki POP/IMAP konta',
|
||||
'Rule not found' => 'Nie znaleziono reguły',
|
||||
'Sending period' => 'Okres wysyłki',
|
||||
'Scope' => 'Zakres',
|
||||
'Actions' => 'Akcje',
|
||||
'Dynamic' => 'Dynamiczny',
|
||||
'Snapshot' => 'Migawka',
|
||||
'To' => 'do',
|
||||
'Back' => 'Powrót',
|
||||
'Rule details' => 'Szczegóły reguły',
|
||||
'Selected' => 'Wybrano',
|
||||
'Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.' => 'Usunięcie reguły wyłączy odpowiedź automatyczną utworzoną przez tę regułę dla objętych skrzynek.',
|
||||
];
|
||||
|
||||
@@ -8,10 +8,10 @@ DEFAULT_ENABLE=true
|
||||
# Strefa czasu używana do interpretacji i wyświetlania dat rozpoczęcia oraz zakończenia.
|
||||
GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw
|
||||
|
||||
# Tryb harmonogramu.
|
||||
# exact_hour = wybór dnia i godziny.
|
||||
# directadmin_period = wybór dnia oraz morning/afternoon/evening zgodnie z API DirectAdmin.
|
||||
GLOBAL_AUTORESPONDER_SCHEDULE_MODE=exact_hour
|
||||
# Tryb pracy pluginu.
|
||||
# da = użycie natywnych autoresponderów DirectAdmin.
|
||||
# exim = własna obsługa z możliwością podania dokładnej godziny i minuty.
|
||||
GLOBAL_AUTORESPONDER_BACKEND=da
|
||||
|
||||
# Czy odpowiadać automatycznie na każdą kwalifikującą się wiadomość.
|
||||
GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@ name=global-autoresponder
|
||||
id=global-autoresponder
|
||||
type=user
|
||||
author=HITME.PL
|
||||
version=1.0.2
|
||||
version=1.0.3
|
||||
active=no
|
||||
installed=no
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once PLUGIN_ROOT . '/exec/lib/Settings.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/BackendRuntimeConfig.php';
|
||||
|
||||
test('backend runtime sync invokes backend script when setting changes', function (): void {
|
||||
$settingsPath = TEST_TMP . '/backend-runtime.conf';
|
||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||
$settings = Settings::load($settingsPath);
|
||||
|
||||
$script = TEST_TMP . '/backend.sh';
|
||||
test_write($script, "#!/bin/sh\necho \"$1\" >> " . escapeshellarg(TEST_TMP . '/called.log') . "\nmkdir -p " . escapeshellarg(TEST_TMP . '/config') . "\nprintf '{\"active_backend\":\"%s\"}\\n' \"$1\" > " . escapeshellarg(TEST_TMP . '/config/backend-state.json') . "\n");
|
||||
chmod($script, 0700);
|
||||
|
||||
BackendRuntimeConfig::sync($settings, $script, TEST_TMP . '/config/backend-state.json');
|
||||
assert_same("exim\n", file_get_contents(TEST_TMP . '/called.log'));
|
||||
|
||||
BackendRuntimeConfig::sync($settings, $script, TEST_TMP . '/config/backend-state.json');
|
||||
assert_same("exim\n", file_get_contents(TEST_TMP . '/called.log'), 'Script should not run again when backend is already active');
|
||||
});
|
||||
@@ -10,7 +10,7 @@ test('settings validate defaults and external paths', function (): void {
|
||||
'DEFAULT_PLUGIN_LANGUAGE=pl',
|
||||
'DEFAULT_ENABLE=false',
|
||||
'GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw',
|
||||
'GLOBAL_AUTORESPONDER_SCHEDULE_MODE=exact_hour',
|
||||
'GLOBAL_AUTORESPONDER_BACKEND=exim',
|
||||
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false',
|
||||
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES=60',
|
||||
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE=true',
|
||||
@@ -23,6 +23,7 @@ test('settings validate defaults and external paths', function (): void {
|
||||
assert_false($settings->defaultEnable());
|
||||
assert_same('Europe/Warsaw', $settings->timezone());
|
||||
assert_same('exact_hour', $settings->scheduleMode());
|
||||
assert_same('exim', $settings->backendMode());
|
||||
assert_same(60, $settings->repeatMinutes());
|
||||
assert_same('/usr/local/hitme_plugins/global-autoresponders/config', Settings::CONFIG_DIR);
|
||||
assert_same('/usr/local/hitme_plugins/global-autoresponders/data', Settings::DATA_DIR);
|
||||
@@ -30,7 +31,7 @@ test('settings validate defaults and external paths', function (): void {
|
||||
|
||||
test('settings reject invalid timezone and schedule mode', function (): void {
|
||||
$path = TEST_TMP . '/bad-settings.conf';
|
||||
test_write($path, "GLOBAL_AUTORESPONDER_TIMEZONE=Not/AZone\nGLOBAL_AUTORESPONDER_SCHEDULE_MODE=bad\n");
|
||||
test_write($path, "GLOBAL_AUTORESPONDER_TIMEZONE=Not/AZone\nGLOBAL_AUTORESPONDER_BACKEND=bad\n");
|
||||
try {
|
||||
Settings::load($path);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
@@ -40,6 +41,19 @@ test('settings reject invalid timezone and schedule mode', function (): void {
|
||||
throw new RuntimeException('Expected invalid settings to fail');
|
||||
});
|
||||
|
||||
test('backend mode derives schedule mode', function (): void {
|
||||
$path = TEST_TMP . '/backend.conf';
|
||||
test_write($path, "GLOBAL_AUTORESPONDER_BACKEND=da\n");
|
||||
$settings = Settings::load($path);
|
||||
assert_same('da', $settings->backendMode());
|
||||
assert_same('directadmin_period', $settings->scheduleMode());
|
||||
|
||||
test_write($path, "GLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||
$settings = Settings::load($path);
|
||||
assert_same('exim', $settings->backendMode());
|
||||
assert_same('exact_hour', $settings->scheduleMode());
|
||||
});
|
||||
|
||||
test('directadmin user access follows DEFAULT_ENABLE and per-user overrides', function (): void {
|
||||
$settingsPath = TEST_TMP . '/access.conf';
|
||||
test_write($settingsPath, "DEFAULT_ENABLE=false\n");
|
||||
|
||||
@@ -28,3 +28,51 @@ test('directadmin user hooks are clickable and point to reserved icon path', fun
|
||||
assert_contains('<a href="/CMD_PLUGINS/global-autoresponder/index.html"', $txt);
|
||||
assert_contains('Globalne autorespondery', $txt);
|
||||
});
|
||||
|
||||
test('rule form uses minute time input and no summary box in exim mode', function (): void {
|
||||
require_once PLUGIN_ROOT . '/exec/lib/Settings.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/Lang.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/DirectAdminUser.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/CsrfGuard.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/RuleRepository.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/AuditLog.php';
|
||||
require_once PLUGIN_ROOT . '/exec/lib/AppContext.php';
|
||||
require_once PLUGIN_ROOT . '/exec/handlers/_form_helpers.php';
|
||||
|
||||
$settingsPath = TEST_TMP . '/form.conf';
|
||||
test_write($settingsPath, "DEFAULT_PLUGIN_LANGUAGE=pl\nGLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||
$settings = Settings::load($settingsPath);
|
||||
$ctx = new AppContext(
|
||||
new DirectAdminUser('alice', 'enhanced', 'pl', TEST_TMP . '/config'),
|
||||
$settings,
|
||||
new RuleRepository(TEST_TMP . '/data'),
|
||||
new CsrfGuard('secret', 'alice', 'sid'),
|
||||
Lang::load('pl', $settings),
|
||||
new AuditLog(TEST_TMP . '/audit.log')
|
||||
);
|
||||
|
||||
$html = render_rule_form($ctx, 'create', null);
|
||||
assert_contains('type="time"', $html);
|
||||
assert_contains('step="60"', $html);
|
||||
assert_false(strpos($html, 'summary-card') !== false, 'Summary box should not be rendered in create/edit form');
|
||||
assert_contains('Temat', $html);
|
||||
assert_contains('Treść wiadomości', $html);
|
||||
});
|
||||
|
||||
test('rule form uses native period selector in da mode', function (): void {
|
||||
$settingsPath = TEST_TMP . '/form-da.conf';
|
||||
test_write($settingsPath, "DEFAULT_PLUGIN_LANGUAGE=pl\nGLOBAL_AUTORESPONDER_BACKEND=da\n");
|
||||
$settings = Settings::load($settingsPath);
|
||||
$ctx = new AppContext(
|
||||
new DirectAdminUser('alice', 'enhanced', 'pl', TEST_TMP . '/config'),
|
||||
$settings,
|
||||
new RuleRepository(TEST_TMP . '/data'),
|
||||
new CsrfGuard('secret', 'alice', 'sid'),
|
||||
Lang::load('pl', $settings),
|
||||
new AuditLog(TEST_TMP . '/audit.log')
|
||||
);
|
||||
$html = render_rule_form($ctx, 'create', null);
|
||||
assert_contains('name="start_value_period"', $html);
|
||||
assert_contains('Rano', $html);
|
||||
assert_false(strpos($html, 'type="time"') !== false);
|
||||
});
|
||||
|
||||
@@ -6,22 +6,37 @@ require_once PLUGIN_ROOT . '/exec/lib/RuleValidator.php';
|
||||
|
||||
test('rule validator normalizes exact hour schedule', function (): void {
|
||||
$settingsPath = TEST_TMP . '/validator.conf';
|
||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw\nGLOBAL_AUTORESPONDER_SCHEDULE_MODE=exact_hour\n");
|
||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw\nGLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||
$settings = Settings::load($settingsPath);
|
||||
$rule = RuleValidator::validate([
|
||||
'subject' => 'Urlop',
|
||||
'body' => "Dzień dobry\r\nWracam jutro",
|
||||
'start_value' => '2026-06-10T09:00',
|
||||
'end_value' => '2026-06-24T17:00',
|
||||
'start_value' => '2026-06-10T09:37',
|
||||
'end_value' => '2026-06-24T17:12',
|
||||
'enabled' => '1',
|
||||
], $settings);
|
||||
|
||||
assert_same('Urlop', $rule['subject']);
|
||||
assert_same("Dzień dobry\nWracam jutro", $rule['body']);
|
||||
assert_same('exact_hour', $rule['schedule_mode']);
|
||||
assert_same('2026-06-10T09:37', $rule['start_value']);
|
||||
assert_true($rule['start_ts'] < $rule['end_ts']);
|
||||
});
|
||||
|
||||
test('rule validator accepts da period schedule when backend is da', function (): void {
|
||||
$settingsPath = TEST_TMP . '/validator-da.conf';
|
||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=da\n");
|
||||
$settings = Settings::load($settingsPath);
|
||||
$rule = RuleValidator::validate([
|
||||
'subject' => 'Urlop',
|
||||
'body' => 'Body',
|
||||
'start_value' => '2026-06-10|morning',
|
||||
'end_value' => '2026-06-24|evening',
|
||||
'enabled' => '1',
|
||||
], $settings);
|
||||
assert_same('directadmin_period', $rule['schedule_mode']);
|
||||
});
|
||||
|
||||
test('rule validator rejects header injection and reversed dates', function (): void {
|
||||
$settings = Settings::load(TEST_TMP . '/missing.conf');
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user