- shared/platform-tools: ea3616a → 12f26fd (27 commits). Traz o módulo de audit (f761d99, o pin da frota), «manter sessão iniciada», o registo de segurança da frota (o gancho que o login já tinha passa a funcionar) e a correção do IP do cliente encontrada nesta auditoria (último X-Forwarded-For / X-Real-IP), promovida ao partilhado. client_ip() da app delega nele. - config/functions_audit.php: audit() sobre platform_audit(); tabela audit_log criada no primeiro uso, segredos ocultados, nunca parte a ação. - Registado: login, falha de login, bloqueio por excesso de tentativas, logout, pedidos de recuperação, mudança/definição de palavra-passe, registo e criação de utilizadores, convites, perfil (antes/depois), todas as definições (db_update_setting, com valor anterior), cada passo da lixeira (apagar, restaurar, eliminar de vez) e ações nas mensagens. - Sistema → Auditoria (views/sysadmin/audit.php), só sysadmin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
313 lines
17 KiB
PHP
313 lines
17 KiB
PHP
<?php
|
|
require_once __DIR__ . '/functions_settings.php'; // platform_admin_url()
|
|
// ─── Shared SMTP status view ─────────────────────────────────────────────────
|
|
//
|
|
// Bootstrap contract — the stub at <host>/views/sysadmin/smtp_status.php must:
|
|
// 1. Load $pdo 2. Run the sysadmin guard 3. Set $repo_root
|
|
// Optional overrides, same as backups.php / platform_update.php:
|
|
// $platform_layout_header / _navbar / _footer, $platform_dashboard_url
|
|
//
|
|
// Read-only by default: it reports what the transport WOULD do. The two actions
|
|
// (connection test, send a test message) are explicit and CSRF-guarded, because
|
|
// one opens a socket to a third party and the other actually sends mail.
|
|
|
|
require_once __DIR__ . '/functions_mail.php';
|
|
require_once __DIR__ . '/functions_csrf.php';
|
|
|
|
if (!isset($repo_root) || !is_string($repo_root)) {
|
|
$repo_root = realpath(dirname(__DIR__, 2)) ?: dirname(__DIR__, 2);
|
|
}
|
|
$repo_root = rtrim($repo_root, '/');
|
|
|
|
// ── Actions (POST to self, PRG) ─────────────────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$action = $_POST['action'] ?? '';
|
|
if (!platform_csrf_ok()) {
|
|
$_SESSION['smtp_flash'] = ['type' => 'danger', 'message' => 'Pedido rejeitado (token CSRF ausente ou inválido).'];
|
|
} elseif ($action === 'test_connection') {
|
|
$r = platform_mail_test($repo_root);
|
|
$_SESSION['smtp_flash'] = $r['ok']
|
|
? ['type' => 'success', 'message' => 'Ligação SMTP estabelecida e fechada com sucesso.', 'log' => $r['log']]
|
|
: ['type' => 'danger', 'message' => 'Falha na ligação: ' . $r['error'], 'log' => $r['log']];
|
|
} elseif ($action === 'save') {
|
|
// Editar aqui, e não só diagnosticar.
|
|
//
|
|
// A página nasceu só de leitura, o que fazia sentido enquanto cada site
|
|
// tivesse a sua página de Comunicações. Não têm: no hub2, no silver e no
|
|
// quack esta é a única página de SMTP que existe, e a única forma de
|
|
// mudar o servidor era o setup.php -- uma página chamada "Configuração
|
|
// inicial", que fecha assim que houver um sysadmin. Uma página que
|
|
// mostra "Servidor: — vazio —" sem dar maneira de o preencher é um beco.
|
|
//
|
|
// Os sites que TÊM Comunicações próprias redirigem daqui para lá, para
|
|
// não haver dois editores da mesma coisa.
|
|
if (!isset($pdo) || !$pdo instanceof PDO) {
|
|
// Sem base não há onde gravar. O stub deste site só forneceu
|
|
// $repo_root, por isso a página fica de leitura.
|
|
$_SESSION['smtp_flash'] = ['type' => 'danger',
|
|
'message' => 'Esta instalação não expõe a base de dados a esta página; configure pelo .env.'];
|
|
header('Location: ' . $_SERVER['REQUEST_URI'], true, 303);
|
|
exit;
|
|
}
|
|
$uid = isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : null;
|
|
|
|
$port = (int) ($_POST['smtp_port'] ?? 587);
|
|
$secure = strtolower(trim((string) ($_POST['smtp_secure'] ?? 'tls')));
|
|
if (!in_array($secure, ['tls', 'ssl', ''], true)) {
|
|
$secure = 'tls';
|
|
}
|
|
|
|
$from = trim((string) ($_POST['email_from'] ?? ''));
|
|
if ($from !== '' && !filter_var($from, FILTER_VALIDATE_EMAIL)) {
|
|
// Validado porque já se perdeu tempo com isto: o campo do remetente
|
|
// ficou uma vez com o NOME do remetente lá dentro, e o erro só
|
|
// apareceu como "Invalid address (From)" numa fila presa, horas
|
|
// depois. Um formulário que aceita tudo empurra a falha para longe
|
|
// de quem a pode corrigir.
|
|
$_SESSION['smtp_flash'] = ['type' => 'danger',
|
|
'message' => 'O remetente tem de ser um endereço de email — o nome vai no campo abaixo.'];
|
|
header('Location: ' . $_SERVER['REQUEST_URI'], true, 303);
|
|
exit;
|
|
}
|
|
|
|
platform_settings_set($pdo, 'smtp_host', trim((string) ($_POST['smtp_host'] ?? '')), $uid);
|
|
platform_settings_set($pdo, 'smtp_port', (string) ($port > 0 ? $port : 587), $uid);
|
|
platform_settings_set($pdo, 'smtp_secure', $secure, $uid);
|
|
platform_settings_set($pdo, 'smtp_username', trim((string) ($_POST['smtp_username'] ?? '')), $uid);
|
|
platform_settings_set($pdo, 'email_from', $from, $uid);
|
|
platform_settings_set($pdo, 'email_from_name', trim((string) ($_POST['email_from_name'] ?? '')), $uid);
|
|
|
|
// A password nunca é pré-preenchida, por isso vazio significa "mantém a
|
|
// que está" e não "apaga". Apagar é uma caixa explícita.
|
|
if (!empty($_POST['smtp_password_clear'])) {
|
|
platform_settings_set($pdo, 'smtp_password', '', $uid);
|
|
} elseif (trim((string) ($_POST['smtp_password'] ?? '')) !== '') {
|
|
platform_settings_set($pdo, 'smtp_password', trim((string) $_POST['smtp_password']), $uid);
|
|
}
|
|
|
|
if (function_exists('platform_audit')) {
|
|
platform_audit($pdo, 'settings.smtp', ['target_type' => 'settings', 'summary' => 'Configuração SMTP guardada',
|
|
'changes' => ['smtp_host' => $_POST['smtp_host'] ?? null, 'smtp_port' => $_POST['smtp_port'] ?? null,
|
|
'smtp_username' => $_POST['smtp_username'] ?? null, 'email_from' => $_POST['email_from'] ?? null,
|
|
'smtp_password' => ($_POST['smtp_password'] ?? '') !== '' ? 'alterada' : null]]);
|
|
}
|
|
$_SESSION['smtp_flash'] = ['type' => 'success', 'message' => 'Configuração guardada.'];
|
|
} elseif ($action === 'send_test') {
|
|
$to = trim((string) ($_POST['to'] ?? ''));
|
|
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
|
$_SESSION['smtp_flash'] = ['type' => 'danger', 'message' => 'Endereço de destino inválido.'];
|
|
} else {
|
|
$r = platform_mail_send([
|
|
'to' => $to,
|
|
'to_name' => 'Teste',
|
|
'subject' => 'Teste de SMTP — ' . (string) ($_SERVER['HTTP_HOST'] ?? 'plataforma'),
|
|
'body' => '<p>Mensagem de teste enviada a partir do painel de sistema.</p>',
|
|
'alt_body' => 'Mensagem de teste enviada a partir do painel de sistema.',
|
|
], $repo_root);
|
|
if (!empty($r['skipped'])) {
|
|
$_SESSION['smtp_flash'] = ['type' => 'warning',
|
|
'message' => 'Não enviado: EMAIL_DEV_PRINT está activo e APP_ENV não é prod, por isso a mensagem foi apenas registada no log.'];
|
|
} elseif (!empty($r['ok'])) {
|
|
$_SESSION['smtp_flash'] = ['type' => 'success', 'message' => 'Mensagem entregue ao servidor SMTP para ' . $to . '.'];
|
|
} else {
|
|
$_SESSION['smtp_flash'] = ['type' => 'danger', 'message' => 'Falha no envio: ' . $r['error']];
|
|
}
|
|
}
|
|
}
|
|
header('Location: ' . $_SERVER['REQUEST_URI'], true, 303);
|
|
exit;
|
|
}
|
|
|
|
// Editar exige base de dados: é lá que as definições ficam. Um stub que só
|
|
// forneça $repo_root continua a ver a página, em modo de leitura.
|
|
$canEdit = isset($pdo) && $pdo instanceof PDO;
|
|
|
|
$flash = $_SESSION['smtp_flash'] ?? null;
|
|
unset($_SESSION['smtp_flash']);
|
|
|
|
// A configuração que realmente envia, e não a do ambiente: com $pdo, é a da
|
|
// base que ganha. Era daqui que vinha o formulário mostrar campos vazios num
|
|
// site cujo email funcionava.
|
|
$cfg = (isset($pdo) && $pdo instanceof PDO && function_exists('platform_smtp_config'))
|
|
? platform_mail_config(platform_smtp_config($pdo))
|
|
: platform_mail_config();
|
|
$hasMailer = platform_mail_available($repo_root);
|
|
$isProd = ($cfg['env'] === 'prod');
|
|
|
|
// Veredicto e valores em vigor: uma implementação só, em functions_mail.php.
|
|
// Estavam aqui em linha, e quando o comms.php passou a mostrar o mesmo passaram
|
|
// a ser duas cópias da mesma lista de casos -- a que ficasse para trás mentiria
|
|
// a alguém a meio de uma avaria.
|
|
$verdict = platform_mail_diagnosis($repo_root, null, $pdo ?? null);
|
|
$rows = platform_mail_config_rows(null, $pdo ?? null);
|
|
|
|
$layout_header = $platform_layout_header ?? ($repo_root . '/includes/header.php');
|
|
$layout_navbar = $platform_layout_navbar ?? ($repo_root . '/includes/navbar.php');
|
|
$layout_footer = $platform_layout_footer ?? ($repo_root . '/includes/footer.php');
|
|
$dashboard_url = $platform_dashboard_url
|
|
?? (function_exists('base_url') ? platform_admin_url('index.php') : 'index.php');
|
|
|
|
$pageTitle = 'Estado SMTP';
|
|
if (is_file($layout_header)) include $layout_header;
|
|
if (is_file($layout_navbar)) include $layout_navbar;
|
|
?>
|
|
<div class="container py-4" style="max-width:900px;">
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<div>
|
|
<h2 class="mb-1"><i class="bi bi-envelope-check me-2"></i>Estado SMTP</h2>
|
|
<p class="text-muted small mb-0">Configuração do transporte de email partilhado e testes de ligação.</p>
|
|
</div>
|
|
<a href="<?= htmlspecialchars($dashboard_url) ?>" class="btn btn-outline-secondary btn-sm">Voltar</a>
|
|
</div>
|
|
|
|
<?php if (is_array($flash)): ?>
|
|
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
|
|
<?= htmlspecialchars((string) $flash['message']) ?>
|
|
<?php if (!empty($flash['log'])): ?>
|
|
<pre class="small mt-2 mb-0" style="max-height:220px;overflow:auto;white-space:pre-wrap;"><?= htmlspecialchars((string) $flash['log']) ?></pre>
|
|
<?php endif; ?>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card shadow-sm mb-3 border-<?= htmlspecialchars($verdict[0]) ?>">
|
|
<div class="card-body d-flex align-items-start gap-3">
|
|
<span class="badge text-bg-<?= htmlspecialchars($verdict[0]) ?> mt-1"><?= htmlspecialchars($verdict[1]) ?></span>
|
|
<div class="small mb-0"><?= htmlspecialchars($verdict[2]) ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<strong><i class="bi bi-sliders me-1"></i>Configuração</strong>
|
|
<span class="badge text-bg-<?= $hasMailer ? 'success' : 'danger' ?>">
|
|
PHPMailer <?= $hasMailer ? 'disponível' : 'em falta' ?>
|
|
</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0 small align-middle">
|
|
<tbody>
|
|
<?php foreach ($rows as [$k, $v, $ok]): ?>
|
|
<tr>
|
|
<td class="font-monospace text-muted" style="width:220px;"><?= htmlspecialchars($k) ?></td>
|
|
<td class="font-monospace text-break"><?= htmlspecialchars($v) ?></td>
|
|
<td class="text-end" style="width:40px;">
|
|
<i class="bi bi-<?= $ok ? 'check-circle text-success' : 'exclamation-circle text-warning' ?>"></i>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card-footer small text-muted">
|
|
Valores lidos por ordem: constante definida pela aplicação → <code>.env</code>.
|
|
Onde a aplicação guarda definições em base de dados, essas ganham sobre ambas.
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($canEdit): ?>
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header"><strong><i class="bi bi-pencil-square me-1"></i>Alterar</strong></div>
|
|
<div class="card-body">
|
|
<form method="post" class="row g-3">
|
|
<?= platform_csrf_field() ?>
|
|
<input type="hidden" name="action" value="save">
|
|
|
|
<div class="col-md-8">
|
|
<label class="form-label small">Servidor SMTP</label>
|
|
<input type="text" name="smtp_host" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['host']) ?>" placeholder="mail.exemplo.pt">
|
|
<div class="form-text">No cPanel é tipicamente <code>mail.<dominio></code>.</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small">Porta</label>
|
|
<input type="number" name="smtp_port" class="form-control form-control-sm"
|
|
value="<?= (int) $cfg['port'] ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small">Cifra</label>
|
|
<select name="smtp_secure" class="form-select form-select-sm">
|
|
<?php foreach (['tls' => 'TLS', 'ssl' => 'SSL', '' => 'Nenhuma'] as $v => $lbl): ?>
|
|
<option value="<?= $v ?>" <?= $cfg['secure'] === $v ? 'selected' : '' ?>><?= $lbl ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label small">Utilizador</label>
|
|
<input type="text" name="smtp_username" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['username']) ?>" autocomplete="off">
|
|
<div class="form-text">Normalmente o endereço de email completo.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small">Palavra-passe</label>
|
|
<input type="password" name="smtp_password" class="form-control form-control-sm"
|
|
placeholder="<?= $cfg['password'] !== '' ? '•••••••• (definida — deixe vazio para manter)' : 'por definir' ?>"
|
|
autocomplete="new-password">
|
|
<?php if ($cfg['password'] !== ''): ?>
|
|
<div class="form-check mt-1">
|
|
<input class="form-check-input" type="checkbox" name="smtp_password_clear" id="pwClear" value="1">
|
|
<label class="form-check-label small text-muted" for="pwClear">Apagar a palavra-passe guardada</label>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label small">Remetente (From)</label>
|
|
<input type="email" name="email_from" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['from']) ?>" placeholder="noreply@exemplo.pt">
|
|
<div class="form-text">Tem de ser a conta autenticada acima, senão SPF/DMARC rejeitam.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small">Nome do remetente</label>
|
|
<input type="text" name="email_from_name" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['from_name']) ?>" placeholder="A Minha Plataforma">
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary btn-sm">
|
|
<i class="bi bi-check-lg me-1"></i>Guardar
|
|
</button>
|
|
<span class="small text-muted ms-2">
|
|
Guardado em base de dados, que ganha ao <code>.env</code>. Teste a ligação a seguir.
|
|
</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-header"><strong><i class="bi bi-activity me-1"></i>Testes</strong></div>
|
|
<div class="card-body d-flex flex-wrap gap-3 align-items-end">
|
|
<form method="POST" class="m-0">
|
|
<?= platform_csrf_field() ?><input type="hidden" name="action" value="test_connection">
|
|
<button type="submit" class="btn btn-outline-primary btn-sm" <?= $cfg['host'] === '' ? 'disabled' : '' ?>>
|
|
<i class="bi bi-plug me-1"></i>Testar ligação
|
|
</button>
|
|
<div class="form-text">Abre e fecha uma ligação. Não envia nada.</div>
|
|
</form>
|
|
|
|
<form method="POST" class="m-0 d-flex gap-2 align-items-end flex-wrap">
|
|
<?= platform_csrf_field() ?><input type="hidden" name="action" value="send_test">
|
|
<div>
|
|
<label class="form-label small mb-1" for="smtp_to">Enviar mensagem de teste para</label>
|
|
<input type="email" name="to" id="smtp_to" class="form-control form-control-sm"
|
|
style="min-width:260px;" placeholder="alguem@exemplo.pt" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-sm">
|
|
<i class="bi bi-send me-1"></i>Enviar
|
|
</button>
|
|
</form>
|
|
</div>
|
|
<div class="card-footer small text-muted">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
Um teste bem sucedido significa que o servidor SMTP <em>aceitou</em> a mensagem —
|
|
não que ela chegou à caixa de entrada. Rejeições por SPF, DKIM ou reputação acontecem depois.
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<?php if (is_file($layout_footer)) include $layout_footer; ?>
|