- 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>
258 lines
12 KiB
PHP
258 lines
12 KiB
PHP
<?php
|
|
// ─── Página de restauro (partilhada) ────────────────────────────────────────
|
|
//
|
|
// Contraparte da backups.php. O functions_restore.php existia há tempo e estava
|
|
// completo — inspecção, backup de segurança, divisão de statements, remoção de
|
|
// tabelas que o dump não conhece — mas nenhuma página o expunha. Havia backups
|
|
// que ninguém sabia se restauravam, que é o mesmo que não ter backups.
|
|
//
|
|
// Contrato do stub, como o backups.php: $pdo, guarda de sysadmin, $repo_root.
|
|
//
|
|
// Três passos, e não um botão:
|
|
//
|
|
// 1. escolher a lista só mostra artefactos de base de dados
|
|
// 2. inspeccionar o que está dentro do ZIP, antes de se decidir
|
|
// 3. confirmar escrevendo o nome do ficheiro à mão
|
|
//
|
|
// O passo 3 é de propósito. Uma checkbox aprende-se a clicar sem ler; escrever
|
|
// "db_backup_25_08_2026_02_00_11_a3f9c1.zip" obriga a olhar para QUAL artefacto
|
|
// se está prestes a aplicar. Isto apaga a base de dados inteira -- não é o sítio
|
|
// para poupar dois segundos ao utilizador.
|
|
//
|
|
// O restauro tira sempre um backup de segurança do estado actual antes de tocar
|
|
// em nada, e o nome dele aparece no relatório: restaurar o artefacto errado é,
|
|
// ele próprio, recuperável.
|
|
|
|
if (!isset($pdo) || !$pdo instanceof PDO) {
|
|
http_response_code(500);
|
|
exit('restore.php: sem PDO — o arranque do site (config/db.php) tem de abrir a ligação primeiro.');
|
|
}
|
|
|
|
require_once __DIR__ . '/functions_restore.php';
|
|
require_once __DIR__ . '/functions_csrf.php';
|
|
|
|
$selfUrl = strtok((string) ($_SERVER['REQUEST_URI'] ?? ''), '?') ?: '';
|
|
|
|
// ── POST ────────────────────────────────────────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!platform_csrf_ok()) {
|
|
$_SESSION['restore_flash'] = ['type' => 'danger', 'msg' => 'Pedido rejeitado (token CSRF inválido).'];
|
|
header('Location: ' . $selfUrl, true, 303);
|
|
exit;
|
|
}
|
|
|
|
$file = (string) ($_POST['filename'] ?? '');
|
|
$typed = trim((string) ($_POST['confirm_name'] ?? ''));
|
|
$dropExtra = !empty($_POST['drop_extra']);
|
|
|
|
if ($typed !== $file) {
|
|
$_SESSION['restore_flash'] = ['type' => 'warning', 'msg' =>
|
|
'O nome escrito não corresponde ao ficheiro escolhido — nada foi feito.'];
|
|
header('Location: ' . $selfUrl . '?f=' . urlencode($file), true, 303);
|
|
exit;
|
|
}
|
|
|
|
// A partir daqui a base de dados vai ser substituída.
|
|
$report = restore_db($pdo, $file, ['safety' => true, 'drop_extra_tables' => $dropExtra]);
|
|
// Depois do restauro: a tabela do registo também foi reposta, e é na nova que
|
|
// tem de ficar escrito que isto aconteceu.
|
|
if (function_exists('platform_audit')) {
|
|
platform_audit($pdo, 'backup.restore', ['target_type' => 'backup', 'target_label' => $file,
|
|
'summary' => !empty($report['ok']) ? 'Base de dados reposta' : 'Restauro falhou',
|
|
'changes' => ['drop_extra_tables' => $dropExtra]]);
|
|
}
|
|
$_SESSION['restore_report'] = $report;
|
|
header('Location: ' . $selfUrl, true, 303);
|
|
exit;
|
|
}
|
|
|
|
$flash = $_SESSION['restore_flash'] ?? null;
|
|
$report = $_SESSION['restore_report'] ?? null;
|
|
unset($_SESSION['restore_flash'], $_SESSION['restore_report']);
|
|
|
|
$all = backup_list();
|
|
$dbFiles = array_values(array_filter($all, fn($b) => ($b['type'] ?? '') === 'db'));
|
|
$selected = (string) ($_GET['f'] ?? '');
|
|
$inspect = $selected !== '' ? restore_inspect($selected) : null;
|
|
$tables = restore_current_tables($pdo);
|
|
|
|
$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');
|
|
$backupsUrl = $platform_backups_url
|
|
?? (function_exists('platform_admin_url') ? platform_admin_url('backups.php') : 'backups.php');
|
|
|
|
$pageTitle = 'Restaurar base de dados';
|
|
if (is_file($layout_header)) include $layout_header;
|
|
if (is_file($layout_navbar)) include $layout_navbar;
|
|
|
|
$h = fn($s) => htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
?>
|
|
<div class="container py-4" style="max-width:900px;">
|
|
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-4">
|
|
<div>
|
|
<h3 class="fw-bold mb-1"><i class="bi bi-arrow-counterclockwise me-2"></i>Restaurar base de dados</h3>
|
|
<p class="text-muted mb-0">Substitui o esquema e os dados actuais por um artefacto de backup.</p>
|
|
</div>
|
|
<a href="<?= $h($backupsUrl) ?>" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-archive me-1"></i>Backups
|
|
</a>
|
|
</div>
|
|
|
|
<?php if ($flash): ?>
|
|
<div class="alert alert-<?= $h($flash['type']) ?> py-2"><?= $h($flash['msg']) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (is_array($report)): ?>
|
|
<div class="card shadow-sm mb-4 border-<?= $report['ok'] ? 'success' : 'danger' ?>">
|
|
<div class="card-header fw-semibold">
|
|
<i class="bi bi-<?= $report['ok'] ? 'check-circle' : 'x-octagon' ?> me-1"></i>
|
|
<?= $report['ok'] ? 'Restauro concluído' : 'Restauro falhou' ?>
|
|
</div>
|
|
<div class="card-body">
|
|
<table class="table table-sm align-middle mb-0">
|
|
<tbody>
|
|
<tr><td class="text-muted" style="width:14rem;">Artefacto aplicado</td>
|
|
<td><code><?= $h($report['filename']) ?></code></td></tr>
|
|
<tr><td class="text-muted">Backup de segurança</td>
|
|
<td><?= $report['safety_backup']
|
|
? '<code>' . $h($report['safety_backup']) . '</code>'
|
|
: '<span class="text-danger">nenhum</span>' ?></td></tr>
|
|
<tr><td class="text-muted">Statements</td>
|
|
<td><?= (int) $report['applied'] ?> aplicados de <?= (int) $report['statements'] ?></td></tr>
|
|
<?php if (isset($report['tables_before'])): ?>
|
|
<tr><td class="text-muted">Tabelas</td>
|
|
<td><?= (int) $report['tables_before'] ?> antes → <?= (int) $report['tables_after'] ?> depois</td></tr>
|
|
<?php endif; ?>
|
|
<?php if (!empty($report['dropped_extra'])): ?>
|
|
<tr><td class="text-muted">Tabelas removidas</td>
|
|
<td><code><?= $h(implode(', ', $report['dropped_extra'])) ?></code>
|
|
<div class="form-text">Existiam aqui e não no artefacto.</div></td></tr>
|
|
<?php endif; ?>
|
|
<?php if (!empty($report['error'])): ?>
|
|
<tr><td class="text-muted">Erro</td>
|
|
<td class="text-danger"><?= $h($report['error']) ?></td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php if (!$report['ok'] && $report['safety_backup']): ?>
|
|
<p class="form-text mb-0 mt-2">
|
|
O estado anterior está em <code><?= $h($report['safety_backup']) ?></code> —
|
|
restaurá-lo devolve a base ao que era antes desta tentativa.
|
|
</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="alert alert-warning d-flex gap-2 align-items-start">
|
|
<i class="bi bi-exclamation-triangle-fill mt-1"></i>
|
|
<div>
|
|
<strong>Isto é destrutivo.</strong> A base de dados actual
|
|
(<?= count($tables) ?> tabelas) é substituída pela do artefacto. Antes de tocar
|
|
em nada é tirado um backup de segurança do estado actual, cujo nome aparece no
|
|
relatório — mas planeie o restauro para uma altura em que ninguém esteja a usar
|
|
a plataforma.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header fw-semibold"><i class="bi bi-list-ul me-1"></i>Artefactos de base de dados</div>
|
|
<div class="card-body">
|
|
<?php if (!$dbFiles): ?>
|
|
<p class="text-muted mb-0">
|
|
Não há backups de base de dados em <code>backups/</code>.
|
|
Crie um a partir da página de <a href="<?= $h($backupsUrl) ?>">Backups</a>.
|
|
</p>
|
|
<?php else: ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle mb-0">
|
|
<thead><tr><th>Ficheiro</th><th class="text-end">Tamanho</th><th>Criado</th><th></th></tr></thead>
|
|
<tbody>
|
|
<?php foreach ($dbFiles as $b): ?>
|
|
<tr<?= $b['filename'] === $selected ? ' class="table-active"' : '' ?>>
|
|
<td><code class="small"><?= $h($b['filename']) ?></code></td>
|
|
<td class="text-end text-muted small"><?= $h($b['size_human']) ?></td>
|
|
<td class="text-muted small"><?= $h(date('Y-m-d H:i', (int) $b['created_at'])) ?></td>
|
|
<td class="text-end">
|
|
<a class="btn btn-outline-secondary btn-sm"
|
|
href="<?= $h($selfUrl) ?>?f=<?= urlencode($b['filename']) ?>">Inspeccionar</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($inspect !== null): ?>
|
|
<div class="card shadow-sm">
|
|
<div class="card-header fw-semibold"><i class="bi bi-search me-1"></i>Inspecção</div>
|
|
<div class="card-body">
|
|
<?php if (!$inspect['ok']): ?>
|
|
<div class="alert alert-danger py-2 mb-0"><?= $h($inspect['error']) ?></div>
|
|
<?php elseif ($inspect['type'] !== 'db'): ?>
|
|
<div class="alert alert-warning py-2 mb-0">
|
|
<code><?= $h($inspect['filename']) ?></code> é um backup de ficheiros, não de base de dados.
|
|
</div>
|
|
<?php else: ?>
|
|
<table class="table table-sm align-middle">
|
|
<tbody>
|
|
<tr><td class="text-muted" style="width:14rem;">Ficheiro</td>
|
|
<td><code><?= $h($inspect['filename']) ?></code></td></tr>
|
|
<tr><td class="text-muted">Tamanho</td><td><?= $h($inspect['size_human']) ?></td></tr>
|
|
<tr><td class="text-muted">Entradas no ZIP</td><td><?= (int) $inspect['entries'] ?></td></tr>
|
|
<?php if (!empty($inspect['sql_entry'])): ?>
|
|
<tr><td class="text-muted">Dump</td><td><code><?= $h($inspect['sql_entry']) ?></code></td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<hr>
|
|
|
|
<form method="post" class="d-flex flex-column gap-3">
|
|
<?= platform_csrf_field() ?>
|
|
<input type="hidden" name="filename" value="<?= $h($inspect['filename']) ?>">
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="dropExtra" name="drop_extra" value="1" checked>
|
|
<label class="form-check-label" for="dropExtra">
|
|
Remover tabelas que o artefacto não conhece
|
|
<span class="d-block text-muted small">
|
|
Sem isto, uma tabela criada depois deste backup sobrevive e deixa o
|
|
esquema num estado que o artefacto nunca descreveu. Desligue só se
|
|
souber que quer manter alguma.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="form-label small fw-semibold" for="confirmName">
|
|
Para confirmar, escreva o nome do ficheiro
|
|
</label>
|
|
<input type="text" class="form-control font-monospace" id="confirmName"
|
|
name="confirm_name" autocomplete="off" spellcheck="false"
|
|
placeholder="<?= $h($inspect['filename']) ?>">
|
|
<div class="form-text">
|
|
É escrito à mão de propósito: obriga a olhar para qual artefacto está
|
|
prestes a aplicar.
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="bi bi-arrow-counterclockwise me-1"></i>Restaurar esta base de dados
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
<?php if (is_file($layout_footer)) include $layout_footer; ?>
|