Files
itflow/admin/post/backup.php
johnnyq dd160ef1d7 Rebuild Maintenance > Backup on the Update page's pattern: a status card at the top says when the last backup was made, what kind and when the next scheduled one is due, or instead that one is being made, the last one failed and why, the last good one is over 7 days old, none has been made yet, a queued one is stuck because cron isn't running, or there is no encryption key, with Back up now and Database only under it, and the page reloads itself every 10 seconds while a backup is being made except while an exported master key is showing; the backups list uses the standard list plumbing with sortable headers on date, type, size and status and the standard pager, no longer stops at the newest 100, shows who made each backup or that the schedule did, when it was last downloaded and why one failed, and a row menu copies its path on the server and its SHA-256 checksum, deletes a finished backup, takes a queued one off the queue or removes a failed or missing one, with the card left out when there are no backups; the schedule and retention card shows whether the Backup cron job is on and at what time, with Turn on and Change the time using the Cron page's enable_cron_job handler and cron_edit window; an At a glance column has the last Full Backup and Database Only, the schedule, retention, how many are kept and their size, free space, cron status and the backup folder; the encryption key is masked behind the eye with a copy button; the master key export moves to a new ajax modal, admin/modals/backup/backup_master_key.php, which asks for the password and says an encrypted copy is filed with the backups and every agent is notified, where the old window said the key was not written anywhere; delete_backup in admin/post/backup.php words its audit entry and message by the backup's status; no new CSS or JS; changelog entries added to 26.11.8
2026-11-13 01:14:10 -05:00

130 lines
5.3 KiB
PHP

<?php
/*
* ITFlow - GET/POST request handler for backups
*
* Archives are not built here. Everything on this page records intent and lets
* cron/backup.php do the work - see backupQueue() for why.
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_GET['queue_backup'])) {
validateCSRFToken();
$type = strtolower(trim($_GET['queue_backup']));
$error = null;
$backup_id = backupQueue($mysqli, $type, $session_name ?? 'Unknown User', $error);
if ($backup_id > 0) {
logAudit("Backup", "Queue", ($session_name ?? 'Unknown User') . " queued a " . backupTypeLabel($type));
// Saying "it will start within a minute" when the master cron switch is off is a lie
// the user only discovers by waiting, so check before promising.
$cron_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron FROM settings WHERE company_id = 1"));
if (intval($cron_row['config_enable_cron']) === 0) {
flashAlert(backupTypeLabel($type) . " queued, but cron is switched off in Maintenance > Cron, so it will not start until that is enabled.", 'error');
} else {
flashAlert(backupTypeLabel($type) . " queued - it will start within a minute and you will be notified when it is ready.");
}
} else {
flashAlert($error ?? "Could not queue the backup.", 'error');
}
redirect("backup.php");
}
if (isset($_GET['delete_backup'])) {
validateCSRFToken();
$backup_id = intval($_GET['delete_backup']);
$sql = mysqli_query($mysqli, "SELECT backup_file_name, backup_type, backup_status FROM backups WHERE backup_id = $backup_id");
if (mysqli_num_rows($sql) === 1) {
$row = mysqli_fetch_assoc($sql);
backupDeleteById($mysqli, $backup_id);
// One still waiting has no file yet - taking it off the queue is all there is to it
if ($row['backup_status'] === 'Pending') {
logAudit("Backup", "Delete", ($session_name ?? 'Unknown User') . " took a queued " . backupTypeLabel($row['backup_type']) . " off the queue");
flashAlert(backupTypeLabel($row['backup_type']) . " taken off the queue.");
} else {
logAudit("Backup", "Delete", ($session_name ?? 'Unknown User') . " deleted backup " . escapeSql($row['backup_file_name']));
flashAlert($row['backup_status'] === 'Complete' ? "Backup deleted." : "Backup removed from the list.");
}
} else {
flashAlert("Backup not found.", 'error');
}
redirect("backup.php");
}
if (isset($_POST['edit_backup_settings'])) {
validateCSRFToken();
$retention_days = intval($_POST['config_backup_retention_days']);
$retention_count = intval($_POST['config_backup_retention_count']);
$cron_type = escapeSql($_POST['config_backup_cron_type']);
if ($retention_days < 0) { $retention_days = 0; }
if ($retention_count < 1) { $retention_count = 1; }
// The scheduled job runs without a session, so it can only produce the unattended types
if (!in_array($cron_type, backupUnattendedTypes(), true)) {
$cron_type = BACKUP_TYPE_FULL;
}
mysqli_query($mysqli, "UPDATE settings SET config_backup_retention_days = $retention_days, config_backup_retention_count = $retention_count, config_backup_cron_type = '$cron_type' WHERE company_id = 1");
logAudit("Backup", "Edit", ($session_name ?? 'Unknown User') . " updated the backup settings");
flashAlert("Backup settings saved.");
redirect("backup.php");
}
if (isset($_POST['backup_master_key'])) {
validateCSRFToken();
$password = $_POST['password'];
$sql = mysqli_query($mysqli, "SELECT user_password, user_specific_encryption_ciphertext FROM users WHERE user_id = $session_user_id");
$row = mysqli_fetch_assoc($sql);
if (!$row || !password_verify($password, $row['user_password'])) {
logAudit("Master Key", "Download", ($session_name ?? 'Unknown User') . " attempted to retrieve the master encryption key but failed");
flashAlert("Incorrect password.", 'error');
redirect("backup.php");
}
$site_encryption_master_key = decryptUserSpecificKey($row['user_specific_encryption_ciphertext'], $password);
if (empty($site_encryption_master_key)) {
logAudit("Master Key", "Download", ($session_name ?? 'Unknown User') . " could not unwrap the master encryption key");
flashAlert("Your password is correct but the master key could not be unwrapped from your account.", 'error');
redirect("backup.php");
}
logAudit("Master Key", "Download", ($session_name ?? 'Unknown User') . " retrieved the master encryption key");
appNotify("Master Key", ($session_name ?? 'Unknown User') . " retrieved the master encryption key", "/admin/backup.php");
// Written as an encrypted archive too, so it can be filed with the other backups.
// This is the one type cron can never produce - the key only exists inside a session.
$error = null;
backupCreate($mysqli, BACKUP_TYPE_MASTER_KEY, $session_name ?? 'Unknown User', 'Manual', $error, ['master_key' => $site_encryption_master_key]);
$_SESSION['backup_master_key_reveal'] = $site_encryption_master_key;
if ($error) {
flashAlert("Master key shown below, but the encrypted copy could not be written: $error", 'error');
}
redirect("backup.php");
}