/views/sysadmin/backups.php must: // 1. Load $pdo (via $repo_root/config/db.php + functions.php) // 2. Run the sysadmin auth guard // 3. Set $repo_root // Optional override variables (same as platform_update.php): // $platform_layout_header / $platform_layout_navbar / $platform_layout_footer // $platform_dashboard_url require_once __DIR__ . '/functions_backup.php'; require_once __DIR__ . '/functions_csrf.php'; // Load BACKUP_KEEP_DAYS (and BACKUP_TOKEN) from the worker config if not yet defined. if (!defined('BACKUP_KEEP_DAYS')) { require_once __DIR__ . '/backups/config.php'; } if (!isset($repo_root) || !is_string($repo_root)) { $repo_root = realpath(dirname(__DIR__, 2)) ?: dirname(__DIR__, 2); } $repo_root = rtrim($repo_root, '/'); // ── Apagar backup (POST à própria página, PRG) ────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_backup') { if (!platform_csrf_ok()) { $_SESSION['backup_flash'] = [ 'type' => 'danger', 'message' => 'Pedido rejeitado (token CSRF ausente ou inválido).', ]; } else { $fn = (string) ($_POST['filename'] ?? ''); $dir = backup_dir(); $real = realpath($dir . DIRECTORY_SEPARATOR . $fn); if (!backup_filename_is_safe($fn) || $real === false || !str_starts_with($real, $dir . DIRECTORY_SEPARATOR)) { $_SESSION['backup_flash'] = ['type' => 'danger', 'message' => 'Ficheiro inválido.']; } elseif (@unlink($real)) { $_SESSION['backup_flash'] = ['type' => 'success', 'message' => "Backup {$fn} apagado."]; } else { $_SESSION['backup_flash'] = ['type' => 'danger', 'message' => "Não foi possível apagar {$fn} (permissões?)."]; } } header('Location: ' . $_SERVER['REQUEST_URI'], true, 303); exit; } // ── Restaurar backup (POST à própria página, PRG) ─────────────────────────── // Destrutivo: substitui o estado actual. Exige CSRF + confirmação escrita, e // restore_db()/restore_files() tiram sempre um backup de segurança antes de // escrever, cujo nome é devolvido na flash para servir de "desfazer". if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'restore_backup') { require_once __DIR__ . '/functions_restore.php'; if (!platform_csrf_ok()) { $_SESSION['backup_flash'] = [ 'type' => 'danger', 'message' => 'Pedido rejeitado (token CSRF ausente ou inválido).', ]; } elseif (trim((string) ($_POST['confirm'] ?? '')) !== 'RESTAURAR') { // Guarda contra o clique por reflexo: um confirm() do browser é // demasiado fácil de despachar sem ler. $_SESSION['backup_flash'] = [ 'type' => 'danger', 'message' => 'Confirmação incorrecta — restauro cancelado. Nada foi alterado.', ]; } else { $fn = (string) ($_POST['filename'] ?? ''); $purge = !empty($_POST['purge']); @set_time_limit(0); ignore_user_abort(true); $info = restore_inspect($fn); if (!$info['ok']) { $_SESSION['backup_flash'] = [ 'type' => 'danger', 'message' => 'Restauro recusado: ' . (string) $info['error'], ]; } elseif ($info['type'] === 'db' && !isset($pdo)) { $_SESSION['backup_flash'] = [ 'type' => 'danger', 'message' => 'Restauro da base de dados indisponível: $pdo não foi carregado pelo stub.', ]; } else { $r = ($info['type'] === 'db') ? restore_db($pdo, $fn) : restore_files($fn, ['purge' => $purge]); if (!empty($r['ok'])) { if ($info['type'] === 'db') { $detail = sprintf( '%d statements aplicados. Tabelas: %s → %s.', (int) $r['statements'], $r['tables_before'] ?? '?', $r['tables_after'] ?? '?' ); if (!empty($r['dropped_extra'])) { $detail .= ' Tabelas removidas por não existirem no backup: ' . implode(', ', $r['dropped_extra']) . '.'; } } else { $detail = sprintf( '%d ficheiro(s) restaurado(s)%s.', (int) $r['extracted'], !empty($r['purged']) ? ', ' . (int) $r['purged'] . ' apagado(s)' : '' ); } // A flash é renderizada com htmlspecialchars(), por isso é texto // simples: markup aqui apareceria literalmente ao utilizador, e // escapar aqui daria escape duplo. $undo = !empty($r['safety_backup']) ? ' Estado anterior guardado em ' . (string) $r['safety_backup'] . ' — restaura esse ficheiro para desfazer.' : ''; $_SESSION['backup_flash'] = [ 'type' => 'success', 'message' => 'Restaurado de ' . $fn . '. ' . $detail . $undo, ]; } else { $safe = !empty($r['safety_backup']) ? ' O backup de segurança ' . (string) $r['safety_backup'] . ' foi criado antes da falha — usa-o para voltar atrás.' : ''; $_SESSION['backup_flash'] = [ 'type' => 'danger', 'message' => 'Restauro falhou: ' . (string) ($r['error'] ?? 'erro desconhecido') . $safe, ]; } } } header('Location: ' . $_SERVER['REQUEST_URI'], true, 303); exit; } $flash = $_SESSION['backup_flash'] ?? null; unset($_SESSION['backup_flash']); $backups = backup_list(); // ── Auto-refresh após "backup iniciado" ───────────────────────────────────── // O trigger devolve o nº de backups antes do disparo; enquanto a contagem não // subir, a página recarrega sozinha (limite de tentativas para não ficar em // loop se o worker falhar). $await_count = null; $await_tries = 0; if (is_array($flash) && isset($flash['await_count'])) { $await_count = (int) $flash['await_count']; } elseif (isset($_GET['pt_await'])) { $await_count = (int) $_GET['pt_await']; $await_tries = max(0, (int) ($_GET['pt_tries'] ?? 0)); } $awaiting = $await_count !== null && count($backups) <= $await_count && $await_tries < 20; $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'); $base_url_fn = function_exists('base_url') ? 'base_url' : null; $dashboard_url = $platform_dashboard_url ?? (function_exists('base_url') ? base_url('views/admin/dashboard.php') : 'dashboard.php'); $pageTitle = 'Backups'; if (is_file($layout_header)) include $layout_header; if (is_file($layout_navbar)) include $layout_navbar; ?>
Gerir cópias de segurança dos uploads e da base de dados. O código reconstitui-se a partir do git — só a pasta uploads/ tem informação externa.
AES-256
Os dumps novos saem encriptados. A chave deriva da APP_SECRET_KEY —
perdê-la é perder os backups; guarde-a fora do servidor.
Sem encriptação
Sem APP_SECRET_KEY no .env, os dumps saem em claro —
e levam a tabela de utilizadores, os contactos e os contratos assinados.