Merge branch 'develop' of https://github.com/itflow-org/itflow into develop

This commit is contained in:
Marcus Hill 2025-05-11 11:46:00 +01:00
commit d856685782
7 changed files with 337 additions and 102 deletions

View File

@ -8,7 +8,7 @@ require_once "includes/inc_all_admin.php";
</div>
<div class="card-body" style="text-align: center;">
<div class="alert alert-secondary">If you are unable to back up the entire VM, you'll need to back up the files & database individually. There is no built-in restore. See the <a href="https://docs.itflow.org/backups" target="_blank">docs here</a>.</div>
<a class="btn btn-primary btn-lg p-3" href="post.php?download_database&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download database</a>
<a class="btn btn-primary btn-lg p-3" href="post.php?download_backup&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download Backup</a>
</div>
</div>

View File

@ -24,40 +24,49 @@ ob_start();
</div>
<div class="modal-body bg-white">
<table class="table table-sm table-hover table-borderless">
<?php if ($num_notifications) { ?>
<?php while ($row = mysqli_fetch_array($sql)) {
<?php while ($row = mysqli_fetch_array($sql)) {
$notification_id = intval($row["notification_id"]);
$notification_type = nullable_htmlentities($row["notification_type"]);
$notification_details = nullable_htmlentities($row["notification"]);
$notification_action = nullable_htmlentities(
$row["notification_action"]
);
$notification_timestamp_formated = date(
"M d g:ia",
strtotime($row["notification_timestamp"])
);
$notification_client_id = intval($row["notification_client_id"]);
if (empty($notification_action)) {
$notification_action = "#";
$notification_id = intval($row["notification_id"]);
$notification_type = nullable_htmlentities($row["notification_type"]);
$notification_details = nullable_htmlentities($row["notification"]);
$notification_action = nullable_htmlentities(
$row["notification_action"]
);
$notification_timestamp_formated = date(
"M d g:ia",
strtotime($row["notification_timestamp"])
);
$notification_client_id = intval($row["notification_client_id"]);
if (empty($notification_action)) {
$notification_action = "#";
}
?>
<tr class="notification-item">
<th>
<a class="text-dark" href="<?php echo $notification_action; ?>">
<i class="fas fa-bullhorn mr-2"></i><?php echo $notification_type; ?>
<small class="text-muted float-right">
<?php echo $notification_timestamp_formated; ?>
</small>
<br>
<small class="text-secondary text-wrap"><?php echo $notification_details; ?></small>
</a>
</th>
</tr>
<?php
}
?>
<a class="text-dark dropdown-item px-1" href="<?php echo $notification_action; ?>">
<div>
<span class="text-bold">
<i class="fas fa-bullhorn mr-2"></i><?php echo $notification_type; ?>
</span>
<small class="text-muted float-right">
<?php echo $notification_timestamp_formated; ?>
</small>
</table>
<div class="text-center mt-2">
<button id="prev-btn" class="btn btn-sm btn-outline-secondary mr-2"><i class="fas fa-caret-left"></i></button>
<button id="next-btn" class="btn btn-sm btn-outline-secondary"><i class="fas fa-caret-right"></i></button>
</div>
<small class="text-secondary text-wrap"><?php echo $notification_details; ?></small>
</a>
<?php
}} else { ?>
<?php } else { ?>
<div class="text-center text-secondary py-5">
<i class='far fa-6x fa-bell-slash'></i>
<h3 class="mt-3">No Notifications</h3>
@ -85,4 +94,41 @@ ob_start();
</button>
</div>
<script>
$(document).ready(function () {
var perPage = 5;
var $items = $(".notification-item");
var totalItems = $items.length;
var totalPages = Math.ceil(totalItems / perPage);
var currentPage = 0;
function showPage(page) {
$items.hide().slice(page * perPage, (page + 1) * perPage).show();
$("#prev-btn").prop("disabled", page === 0);
$("#next-btn").prop("disabled", page >= totalPages - 1);
$("#page-indicator").text(`Page ${page + 1} of ${totalPages} (${totalItems} total)`);
}
$("#prev-btn").on("click", function () {
if (currentPage > 0) {
currentPage--;
showPage(currentPage);
}
});
$("#next-btn").on("click", function () {
if (currentPage < totalPages - 1) {
currentPage++;
showPage(currentPage);
}
});
if (totalItems <= perPage) {
$("#prev-btn, #next-btn, #page-indicator").hide();
}
showPage(currentPage);
});
</script>
<?php require_once "../includes/ajax_footer.php";

View File

@ -94,7 +94,7 @@ if (isset($_GET['asset_id'])) {
$ticket_count = mysqli_num_rows($sql_related_tickets);
// Related Recurring Tickets Query
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_tickets.* FROM recurring_tickets
LEFT JOIN recurring_ticket_assets ON recurring_tickets.recurring_ticket_id = recurring_ticket_assets.recurring_ticket_id
WHERE recurring_ticket_asset_id = $asset_id OR recurring_ticket_assets.asset_id = $asset_id
GROUP BY recurring_tickets.recurring_ticket_id

View File

@ -26,7 +26,6 @@ if ($total_found_rows > 5) {
<option <?php if ($user_config_records_per_page == 20) { echo "selected"; } ?> >20</option>
<option <?php if ($user_config_records_per_page == 50) { echo "selected"; } ?> >50</option>
<option <?php if ($user_config_records_per_page == 100) { echo "selected"; } ?> >100</option>
<option <?php if ($user_config_records_per_page == 500) { echo "selected"; } ?> >500</option>
</select>
</form>
</div>

View File

@ -28,37 +28,33 @@
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<?php if(CURRENT_DATABASE_VERSION > '1.4.5' ) { // Check DB Version REMOVE on Decemeber 1st 2024 -Johnny ?>
<!--Custom Nav Link -->
<?php
$sql_custom_links = mysqli_query($mysqli, "SELECT * FROM custom_links WHERE custom_link_location = 2 AND custom_link_archived_at IS NULL
ORDER BY custom_link_order ASC, custom_link_name ASC"
);
<!--Custom Nav Link -->
<?php
$sql_custom_links = mysqli_query($mysqli, "SELECT * FROM custom_links WHERE custom_link_location = 2 AND custom_link_archived_at IS NULL
ORDER BY custom_link_order ASC, custom_link_name ASC"
);
while ($row = mysqli_fetch_array($sql_custom_links)) {
$custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
if ($custom_link_new_tab == 1) {
$target = "target='_blank' rel='noopener noreferrer'";
} else {
$target = "";
}
while ($row = mysqli_fetch_array($sql_custom_links)) {
$custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
if ($custom_link_new_tab == 1) {
$target = "target='_blank' rel='noopener noreferrer'";
} else {
$target = "";
}
?>
?>
<li class="nav-item" title="<?php echo $custom_link_name; ?>">
<a href="<?php echo $custom_link_uri; ?>" <?php echo $target; ?> class="nav-link">
<i class="fas fa-<?php echo $custom_link_icon; ?> nav-icon"></i>
</a>
</li>
<li class="nav-item" title="<?php echo $custom_link_name; ?>">
<a href="<?php echo $custom_link_uri; ?>" <?php echo $target; ?> class="nav-link">
<i class="fas fa-<?php echo $custom_link_icon; ?> nav-icon"></i>
</a>
</li>
<?php } ?>
<!-- End Custom Nav Links -->
<?php } // End DB Check ?>
<?php } ?>
<!-- End Custom Nav Links -->
<!-- New Notifications Dropdown -->
<?php

View File

@ -6,28 +6,24 @@
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_GET['download_database'])) {
validateCSRFToken($_GET['csrf_token']);
require_once "includes/app_version.php";
if (isset($_GET['download_backup'])) {
validateCSRFToken($_GET['csrf_token']);
global $mysqli, $database;
$backupFileName = date('Y-m-d_H-i-s') . '_backup.sql';
$timestamp = date('YmdHis');
$baseName = "itflow_$timestamp";
$sqlFile = "$baseName.sql";
$uploadsZip = "$baseName_uploads.zip";
$finalZip = "$baseName.zip";
$versionFile = "version.txt";
header('Content-Type: application/sql');
header('Content-Disposition: attachment; filename="' . $backupFileName . '"');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// === 1. Generate SQL Dump ===
$sqlContent = "-- UTF-8 + Foreign Key Safe Dump\n";
$sqlContent .= "SET NAMES 'utf8mb4';\n";
$sqlContent .= "SET foreign_key_checks = 0;\n\n";
if (ob_get_level()) ob_end_clean();
flush();
// Start of dump file — charset declaration
echo "-- UTF-8 + Foreign Key Safe Dump\n";
echo "SET NAMES 'utf8mb4';\n";
echo "SET foreign_key_checks = 0;\n\n";
// Get all tables
$tables = [];
$res = $mysqli->query("SHOW TABLES");
while ($row = $res->fetch_row()) {
@ -35,39 +31,102 @@ if (isset($_GET['download_database'])) {
}
foreach ($tables as $table) {
// Table structure
$createRes = $mysqli->query("SHOW CREATE TABLE `$table`");
$createRow = $createRes->fetch_assoc();
$createSQL = array_values($createRow)[1];
echo "\n-- ----------------------------\n";
echo "-- Table structure for `$table`\n";
echo "-- ----------------------------\n";
echo "DROP TABLE IF EXISTS `$table`;\n";
echo $createSQL . ";\n\n";
$sqlContent .= "\n-- ----------------------------\n";
$sqlContent .= "-- Table structure for `$table`\n";
$sqlContent .= "-- ----------------------------\n";
$sqlContent .= "DROP TABLE IF EXISTS `$table`;\n";
$sqlContent .= $createSQL . ";\n\n";
// Table data
$dataRes = $mysqli->query("SELECT * FROM `$table`");
if ($dataRes->num_rows > 0) {
echo "-- Dumping data for table `$table`\n";
$sqlContent .= "-- Dumping data for table `$table`\n";
while ($row = $dataRes->fetch_assoc()) {
$columns = array_map(fn($col) => '`' . $mysqli->real_escape_string($col) . '`', array_keys($row));
$values = array_map(function ($val) use ($mysqli) {
if (is_null($val)) return "NULL";
return "'" . $mysqli->real_escape_string($val) . "'";
return is_null($val) ? "NULL" : "'" . $mysqli->real_escape_string($val) . "'";
}, array_values($row));
echo "INSERT INTO `$table` (" . implode(", ", $columns) . ") VALUES (" . implode(", ", $values) . ");\n";
$sqlContent .= "INSERT INTO `$table` (" . implode(", ", $columns) . ") VALUES (" . implode(", ", $values) . ");\n";
}
echo "\n";
$sqlContent .= "\n";
}
}
//FINAL STEP: Re-enable foreign key checks
echo "\nSET foreign_key_checks = 1;\n";
$sqlContent .= "SET foreign_key_checks = 1;\n";
file_put_contents($sqlFile, $sqlContent);
logAction("Database", "Download", "$session_name downloaded the database.");
$_SESSION['alert_message'] = "Database downloaded";
// === 2. Create uploads.zip ===
function zipFolder($folderPath, $zipFilePath) {
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
die("Cannot open <$zipFilePath>");
}
$folderPath = realpath($folderPath);
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderPath),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
if (!$file->isDir()) {
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($folderPath) + 1);
$zip->addFile($filePath, $relativePath);
}
}
$zip->close();
}
zipFolder("uploads", $uploadsZip);
// === 3. Generate version.txt ===
$commitHash = trim(shell_exec('git log -1 --format=%H'));
$gitBranch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
$versionContent = "ITFlow Backup Metadata\n";
$versionContent .= "-----------------------------\n";
$versionContent .= "Generated: " . date('Y-m-d H:i:s') . "\n";
$versionContent .= "Backup File: $baseName.zip\n";
$versionContent .= "Generated By: $session_name\n";
$versionContent .= "Host: " . gethostname() . "\n";
$versionContent .= "Git Branch: $gitBranch\n";
$versionContent .= "Git Commit: $commitHash\n";
$versionContent .= "ITFlow Version: " . (defined('APP_VERSION') ? APP_VERSION : 'Unknown') . "\n";
$versionContent .= "Database Version: " . (defined('CURRENT_DATABASE_VERSION') ? CURRENT_DATABASE_VERSION : 'Unknown') . "\n";
file_put_contents($versionFile, $versionContent);
// === 4. Combine into final .zip file ===
$final = new ZipArchive();
if ($final->open($finalZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
die("Cannot create final backup zip.");
}
$final->addFile($sqlFile, "db.sql");
$final->addFile($uploadsZip, "uploads.zip");
$final->addFile($versionFile, "version.txt");
$final->close();
// Cleanup temp files before download
unlink($sqlFile);
unlink($uploadsZip);
unlink($versionFile);
// === 5. Serve the zip for download ===
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $finalZip . '"');
header('Content-Length: ' . filesize($finalZip));
flush();
readfile($finalZip);
unlink($finalZip); // remove final zip after serving
logAction("System", "Backup Download", "$session_name downloaded full backup.");
$_SESSION['alert_message'] = "Full backup downloaded.";
exit;
}
@ -104,3 +163,4 @@ if (isset($_POST['backup_master_key'])) {
header("Location: " . $_SERVER["HTTP_REFERER"]);
}
}

154
setup.php
View File

@ -6,7 +6,6 @@ if (file_exists("config.php")) {
}
include "functions.php";
include "includes/database_version.php";
@ -109,6 +108,118 @@ if (isset($_POST['add_database'])) {
}
if (isset($_POST['restore'])) {
if (!isset($_FILES['backup_zip']) || $_FILES['backup_zip']['error'] !== UPLOAD_ERR_OK) {
die("No backup file uploaded or upload failed.");
}
$file = $_FILES['backup_zip'];
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($fileExt !== "zip") {
die("Only .zip files are allowed.");
}
// Save uploaded file temporarily
$backupZip = "restore_" . time() . ".zip";
if (!move_uploaded_file($file["tmp_name"], $backupZip)) {
die("Failed to save uploaded backup file.");
}
$zip = new ZipArchive;
if ($zip->open($backupZip) !== TRUE) {
unlink($backupZip);
die("Failed to open backup zip file.");
}
// Extract to a temp directory
$tempDir = "restore_temp_" . time();
mkdir($tempDir);
if (!$zip->extractTo($tempDir)) {
$zip->close();
unlink($backupZip);
die("Failed to extract backup contents.");
}
$zip->close();
unlink($backupZip);
// === 1. Restore SQL Dump ===
$sqlPath = "$tempDir/db.sql";
if (file_exists($sqlPath)) {
mysqli_query($mysqli, "SET foreign_key_checks = 0");
$tables = mysqli_query($mysqli, "SHOW TABLES");
while ($row = mysqli_fetch_array($tables)) {
mysqli_query($mysqli, "DROP TABLE IF EXISTS `" . $row[0] . "`");
}
mysqli_query($mysqli, "SET foreign_key_checks = 1");
$command = sprintf(
'mysql -h%s -u%s -p%s %s < %s',
escapeshellarg($dbhost),
escapeshellarg($dbusername),
escapeshellarg($dbpassword),
escapeshellarg($database),
escapeshellarg($sqlPath)
);
exec($command, $output, $returnCode);
if ($returnCode !== 0) {
die("SQL import failed. Error code: $returnCode");
}
} else {
die("Missing db.sql in the backup archive.");
}
// === 2. Restore Upload Folder ===
$uploadDir = __DIR__ . "/uploads/";
$uploadsZip = "$tempDir/uploads.zip";
if (file_exists($uploadsZip)) {
$uploads = new ZipArchive;
if ($uploads->open($uploadsZip) === TRUE) {
// Clean existing uploads
foreach (glob($uploadDir . '*') as $item) {
if (is_dir($item)) {
array_map('unlink', glob("$item/*"));
rmdir($item);
} else {
unlink($item);
}
}
$uploads->extractTo($uploadDir);
$uploads->close();
} else {
die("Failed to open uploads.zip in backup.");
}
} else {
die("Missing uploads.zip in the backup archive.");
}
// === 3. Read version.txt (optional log/display)
$versionTxt = "$tempDir/version.txt";
if (file_exists($versionTxt)) {
$versionInfo = file_get_contents($versionTxt);
// You could log it, show it, or ignore it
// e.g. logAction("Backup Restore", "Version Info", $versionInfo);
}
// Cleanup temp restore directory
array_map('unlink', glob("$tempDir/*"));
rmdir($tempDir);
// === 4. Final Setup Stages ===
$myfile = fopen("config.php", "a");
$txt = "\$config_enable_setup = 0;\n\n";
fwrite($myfile, $txt);
fclose($myfile);
$_SESSION['alert_message'] = "Full backup restored successfully.";
// header("Location: login.php");
exit;
}
if (isset($_POST['add_user'])) {
$user_count = mysqli_num_rows(mysqli_query($mysqli,"SELECT COUNT(*) FROM users"));
if ($user_count < 0) {
@ -146,7 +257,7 @@ if (isset($_POST['add_user'])) {
$new_file_name = md5(time() . $file_name) . '.' . $file_extension;
// check if file has one of the following extensions
$allowed_file_extensions = array('jpg', 'gif', 'png');
$allowed_file_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp');
if (in_array($file_extension,$allowed_file_extensions) === false) {
$file_error = 1;
@ -195,9 +306,6 @@ if (isset($_POST['add_company_settings'])) {
$phone = preg_replace("/[^0-9]/", '',$_POST['phone']);
$email = sanitizeInput($_POST['email']);
$website = sanitizeInput($_POST['website']);
$locale = sanitizeInput($_POST['locale']);
$currency_code = sanitizeInput($_POST['currency_code']);
$timezone = sanitizeInput($_POST['timezone']);
mysqli_query($mysqli,"INSERT INTO companies SET company_name = '$name', company_address = '$address', company_city = '$city', company_state = '$state', company_zip = '$zip', company_country = '$country', company_phone = '$phone', company_email = '$email', company_website = '$website', company_locale = '$locale', company_currency = '$currency_code'");
@ -287,7 +395,6 @@ if (isset($_POST['add_company_settings'])) {
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Event', category_type = 'Referral', category_color = 'red'");
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Affiliate', category_type = 'Referral', category_color = 'pink'");
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Client', category_type = 'Referral', category_color = 'lightblue'");
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Influencer', category_type = 'Referral', category_color = 'turquoise'");
// Payment Methods
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Cash', category_type = 'Payment Method', category_color = 'blue'");
@ -855,10 +962,19 @@ if (isset($_POST['add_telemetry'])) {
<h3 class="card-title"><i class="fas fa-fw fa-database mr-2"></i>Step 2 - Connect your Database</h3>
</div>
<div class="card-body">
<?php if (file_exists('config.php')) { ?>
Database is already configured. Any further changes should be made by editing the config.php file,
or deleting it and refreshing this page.
<?php } else { ?>
<?php
if (file_exists('config.php')) {
echo "<p>Database is already configured. Any further changes should be made by editing the <code>config.php</code> file.</p>";
if (@$mysqli) {
echo "<a href='?user' class='btn btn-success text-bold mt-3'>Next Step (User Setup) <i class='fa fa-fw fa-arrow-circle-right ml-2'></i></a>";
} else {
echo "<div class='alert alert-danger mt-3'>Database connection failed. Check <code>config.php</code>.</div>";
}
} else {
?>
<form method="post" autocomplete="off">
<h5>Database Connection Details</h5>
@ -918,6 +1034,24 @@ if (isset($_POST['add_telemetry'])) {
</div>
</div>
<?php } elseif (isset($_GET['restore'])) { ?>
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-fw fa-database mr-2"></i>Step 2.5 - Restore from Backup</h3>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<label>Restore ITFlow Backup (.zip)</label>
<input type="file" name="backup_zip" accept=".zip" required>
<hr>
<button type="submit" name="restore" class="btn btn-primary text-bold">
Restore Backup<i class="fas fa-fw fa-upload ml-2"></i>
</button>
</form>
</div>
</div>
<?php } elseif (isset($_GET['user'])) { ?>
<div class="card card-dark">