Deploying the local-SMTP commit took fitme3070.pt down:
PHP Warning: require_once(.../shared/platform-tools/compat_secrets.php):
Failed to open stream: No such file or directory
in .../config/functions_secrets.php on line 20
config/functions_secrets.php is loaded on every request, so a hard require_once
on a submodule file is a site-wide fatal — public pages included — whenever
shared/platform-tools lags the code that needs it. It lagged: the server is
still on 603ff84, which predates compat_secrets.php, because "Atualizar
submódulo" was itself broken (fixed in 3626080, which the site had to be up to
deploy). A missing file should not be able to close that loop.
Three requires hardened, each to the failure the caller can actually report:
- config/functions_secrets.php: falls back to plaintext passthrough helpers
and logs loudly. secrets_available() then returns false, which is exactly
what the Comunicações page already renders a warning banner for — so
nothing silently pretends to be encrypted while the site keeps serving.
- config/functions_queue.php (_flush_email): throws instead of fataling. A
fatal killed the request outright, so flush_message()'s catch never ran and
the row sat in 'sending' forever with no explanation. It now lands in
last_error where the Monitor page shows it.
- views/sysadmin/comms.php: renders "Submódulo desactualizado" naming the
missing files, rather than a blank 500 on the page you would open to
diagnose mail in the first place.
Verified by loading config/functions_secrets.php with no shared/ directory
present: the bootstrap completes, the warning is logged once, secrets_available()
is false, and setting_secret()/setting_or_env() still return usable values.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
218 lines
12 KiB
PHP
218 lines
12 KiB
PHP
<?php
|
|
// Comunicações — delivery credentials plus the diagnostics a sysadmin needs
|
|
// when mail stops working.
|
|
//
|
|
// Config and tests live on one page deliberately: the loop is "change a
|
|
// setting, test it, read the real error", and splitting that across two screens
|
|
// makes it slower to diagnose.
|
|
//
|
|
// Sysadmin-only: it handles SMTP and Twilio credentials.
|
|
|
|
require __DIR__ . '/../../config/db.php';
|
|
require __DIR__ . '/../../config/functions.php';
|
|
$user = require_sysadmin();
|
|
|
|
// Same reasoning as config/functions_queue.php: a stale submodule should show
|
|
// an explanation, not a blank 500 on the page you would go to in order to
|
|
// diagnose mail.
|
|
$_ptRoot = __DIR__ . '/../../shared/platform-tools/';
|
|
$_ptMissing = [];
|
|
foreach (['functions_mail.php', 'functions_settings.php'] as $_ptFile) {
|
|
is_file($_ptRoot . $_ptFile) ? require_once $_ptRoot . $_ptFile : $_ptMissing[] = $_ptFile;
|
|
}
|
|
if ($_ptMissing) {
|
|
$page_title = 'Comunicações';
|
|
include __DIR__ . '/../../includes/header.php';
|
|
echo '<div class="container" style="max-width:720px;"><div class="alert alert-danger">'
|
|
. '<h1 class="h5">Submódulo desactualizado</h1>'
|
|
. '<p class="mb-1">Faltam em <code>shared/platform-tools/</code>: <code>'
|
|
. htmlspecialchars(implode('</code>, <code>', $_ptMissing)) . '</code>.</p>'
|
|
. '<p class="mb-0">Actualize o submódulo em <a href="' . htmlspecialchars(base_url('views/sysadmin/platform_update.php'))
|
|
. '">Sistema → Actualizações</a> (ou envie a pasta por FTP) e volte a esta página.</p>'
|
|
. '</div></div>';
|
|
include __DIR__ . '/../../includes/footer.php';
|
|
exit;
|
|
}
|
|
|
|
$repo_root = dirname(__DIR__, 2);
|
|
$saved = false;
|
|
$errors = [];
|
|
$test = null;
|
|
|
|
// Secret fields are write-only in the UI and encrypted at rest. Which keys count
|
|
// as secret is decided by platform_settings_is_secret() — anything ending in
|
|
// _password/_secret/_token/_api_key — rather than a list maintained here, so a
|
|
// field added later can't accidentally be stored in the clear.
|
|
$fields = [
|
|
'smtp_host' => ['label' => 'Servidor SMTP', 'type' => 'text', 'hint' => 'Vazio = usa a função mail() do PHP.'],
|
|
'smtp_port' => ['label' => 'Porta', 'type' => 'number', 'hint' => '587 para TLS, 465 para SSL.'],
|
|
'smtp_secure' => ['label' => 'Encriptação', 'type' => 'select', 'options' => ['tls' => 'TLS (StartTLS)', 'ssl' => 'SSL', '' => 'Nenhuma']],
|
|
'smtp_username' => ['label' => 'Utilizador', 'type' => 'text', 'hint' => 'Normalmente o endereço de email completo.'],
|
|
'smtp_password' => ['label' => 'Palavra-passe', 'type' => 'password'],
|
|
'email_from' => ['label' => 'Remetente', 'type' => 'text', 'hint' => 'Endereço que aparece no From.'],
|
|
'email_from_name' => ['label' => 'Nome do remetente', 'type' => 'text'],
|
|
'twilio_sid' => ['label' => 'Account SID', 'type' => 'text', 'hint' => 'Começa por AC…'],
|
|
'twilio_token' => ['label' => 'Auth Token', 'type' => 'password'],
|
|
'twilio_sms_from' => ['label' => 'Remetente SMS', 'type' => 'text', 'hint' => 'E.164 (+351…) ou alfanumérico.'],
|
|
'twilio_whatsapp_from' => ['label' => 'Remetente WhatsApp', 'type' => 'text', 'hint' => 'Número E.164, sem o prefixo whatsapp:.'],
|
|
'sms_default_country_code' => ['label' => 'Indicativo por defeito', 'type' => 'text', 'hint' => 'Aplicado a números guardados sem +.'],
|
|
];
|
|
|
|
$groups = [
|
|
'SMTP' => ['smtp_host', 'smtp_port', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_from', 'email_from_name'],
|
|
'Twilio (SMS / WhatsApp)' => ['twilio_sid', 'twilio_token', 'twilio_sms_from', 'twilio_whatsapp_from', 'sms_default_country_code'],
|
|
];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
csrf_check();
|
|
$action = $_POST['_action'] ?? 'save';
|
|
$uid = (int) $user['id'];
|
|
|
|
if ($action === 'save') {
|
|
foreach ($fields as $key => $meta) {
|
|
$raw = trim((string) ($_POST[$key] ?? ''));
|
|
|
|
if (platform_settings_is_secret($key)) {
|
|
// Blank means "leave the stored one alone" — the field is never
|
|
// pre-filled with the real value, so blank cannot mean "clear".
|
|
// Clearing is an explicit checkbox.
|
|
if (!empty($_POST[$key . '_clear'])) {
|
|
db_update_setting($pdo, $key, '', $uid);
|
|
} elseif ($raw !== '') {
|
|
db_update_setting($pdo, $key, secret_encrypt($raw), $uid);
|
|
}
|
|
continue;
|
|
}
|
|
db_update_setting($pdo, $key, $raw, $uid);
|
|
}
|
|
// db_update_setting() drops fitme3070's file cache; platform_settings_get()
|
|
// keeps its own per-request cache, which must go too or the diagnostics
|
|
// below would render the values from before this save.
|
|
platform_settings_cache_clear();
|
|
$saved = true;
|
|
} elseif ($action === 'test') {
|
|
$test = platform_mail_test($repo_root, platform_smtp_config($pdo));
|
|
}
|
|
}
|
|
|
|
$current = settings_all();
|
|
[$diagClass, $diagTitle, $diagText] = platform_mail_diagnosis($repo_root, null, $pdo);
|
|
$rows = platform_mail_config_rows(null, $pdo);
|
|
|
|
$page_title = 'Comunicações';
|
|
include __DIR__ . '/../../includes/header.php';
|
|
?>
|
|
<div class="container" style="max-width:860px;">
|
|
<h1 class="h3 mb-1">Comunicações</h1>
|
|
<p class="text-muted mb-4">Credenciais de envio e diagnóstico. As palavras-passe são guardadas cifradas.</p>
|
|
|
|
<?php if ($saved): ?><div class="alert alert-success">Definições guardadas.</div><?php endif; ?>
|
|
<?php foreach ($errors as $e): ?><div class="alert alert-danger"><?= htmlspecialchars($e) ?></div><?php endforeach; ?>
|
|
|
|
<?php if (!platform_settings_secret_available()): ?>
|
|
<div class="alert alert-warning">
|
|
<strong>Cifra indisponível.</strong> Defina <code>APP_SECRET_KEY</code> no <code>.env</code>
|
|
(ou um <code>BACKUP_TOKEN</code> que não seja <code>replace_me</code>) — sem chave, as palavras-passe
|
|
ficam guardadas em texto simples e acabam nos backups da base de dados.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- ─── Estado ─────────────────────────────────────────────────────── -->
|
|
<div class="card shadow-sm mb-3"><div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
|
|
<div>
|
|
<h2 class="h6 mb-1">Estado</h2>
|
|
<span class="badge text-bg-<?= htmlspecialchars($diagClass) ?>"><?= htmlspecialchars($diagTitle) ?></span>
|
|
<p class="text-muted small mb-0 mt-2"><?= htmlspecialchars($diagText) ?></p>
|
|
</div>
|
|
<form method="POST" class="ms-auto">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="_action" value="test">
|
|
<button class="btn btn-outline-primary"><i class="bi bi-plug me-1"></i>Testar ligação SMTP</button>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if ($test !== null): ?>
|
|
<hr>
|
|
<div class="alert alert-<?= !empty($test['ok']) ? 'success' : 'danger' ?> py-2 small mb-2">
|
|
<?= !empty($test['ok']) ? 'Ligação estabelecida.' : htmlspecialchars((string) ($test['error'] ?? 'Falhou.')) ?>
|
|
</div>
|
|
<?php if (!empty($test['log'])): ?>
|
|
<details>
|
|
<summary class="small text-muted">Registo da ligação</summary>
|
|
<pre class="small bg-light p-2 mt-2 mb-0" style="max-height:260px;overflow:auto;white-space:pre-wrap;"><?= htmlspecialchars((string) $test['log']) ?></pre>
|
|
</details>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
|
|
<hr>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<tbody>
|
|
<?php foreach ($rows as [$label, $value, $ok]): ?>
|
|
<tr>
|
|
<th class="text-muted fw-normal" style="width:40%;"><?= htmlspecialchars($label) ?></th>
|
|
<td>
|
|
<?= htmlspecialchars((string) $value) ?>
|
|
<?php if (!$ok): ?><i class="bi bi-exclamation-triangle-fill text-warning ms-1"></i><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p class="text-muted small mb-0 mt-2">
|
|
Valores pinados no <code>.env</code> têm precedência sobre estes campos — se editar aqui e nada mudar,
|
|
é porque a chave está definida no <code>.env</code>.
|
|
</p>
|
|
</div></div>
|
|
|
|
<!-- ─── Definições ─────────────────────────────────────────────────── -->
|
|
<form method="POST">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="_action" value="save">
|
|
<?php foreach ($groups as $groupLabel => $keys): ?>
|
|
<div class="card shadow-sm mb-3"><div class="card-body">
|
|
<h2 class="h6 mb-3"><?= htmlspecialchars($groupLabel) ?></h2>
|
|
<?php foreach ($keys as $key):
|
|
$meta = $fields[$key];
|
|
$isSecret = platform_settings_is_secret($key);
|
|
$stored = (string) ($current[$key] ?? '');
|
|
?>
|
|
<div class="mb-3">
|
|
<label class="form-label" for="f-<?= htmlspecialchars($key) ?>"><?= htmlspecialchars($meta['label']) ?></label>
|
|
<?php if ($meta['type'] === 'select'): ?>
|
|
<select class="form-select" id="f-<?= htmlspecialchars($key) ?>" name="<?= htmlspecialchars($key) ?>">
|
|
<?php foreach ($meta['options'] as $val => $lbl): ?>
|
|
<option value="<?= htmlspecialchars((string) $val) ?>" <?= $stored === (string) $val ? 'selected' : '' ?>><?= htmlspecialchars($lbl) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php elseif ($isSecret): ?>
|
|
<input type="password" class="form-control" id="f-<?= htmlspecialchars($key) ?>"
|
|
name="<?= htmlspecialchars($key) ?>" autocomplete="new-password"
|
|
placeholder="<?= $stored !== '' ? '•••••••• (guardada — deixe vazio para manter)' : 'não definida' ?>">
|
|
<?php if ($stored !== ''): ?>
|
|
<div class="form-check small mt-1">
|
|
<input class="form-check-input" type="checkbox" value="1"
|
|
id="c-<?= htmlspecialchars($key) ?>" name="<?= htmlspecialchars($key) ?>_clear">
|
|
<label class="form-check-label text-muted" for="c-<?= htmlspecialchars($key) ?>">Apagar a guardada</label>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<input type="<?= htmlspecialchars($meta['type']) ?>" class="form-control"
|
|
id="f-<?= htmlspecialchars($key) ?>" name="<?= htmlspecialchars($key) ?>"
|
|
value="<?= htmlspecialchars($stored) ?>">
|
|
<?php endif; ?>
|
|
<?php if (!empty($meta['hint'])): ?>
|
|
<div class="form-text"><?= htmlspecialchars($meta['hint']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div></div>
|
|
<?php endforeach; ?>
|
|
<button class="btn btn-primary">Guardar</button>
|
|
<a class="btn btn-link" href="<?= base_url('views/sysadmin/messages.php') ?>">Ver fila de mensagens →</a>
|
|
</form>
|
|
</div>
|
|
<?php include __DIR__ . '/../../includes/footer.php'; ?>
|