2024-06-28 05:30:49 +01:00
|
|
|
|
<?php
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Set security headers (must be sent before output)
|
|
|
|
|
header('Content-Type: text/html; charset=UTF-8');
|
|
|
|
|
header("Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; object-src 'none'; frame-ancestors 'none'");
|
|
|
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
|
header('Referrer-Policy: no-referrer');
|
|
|
|
|
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
|
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
|
|
|
header('Pragma: no-cache');
|
|
|
|
|
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
|
|
|
|
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper for safe HTML encoding
|
|
|
|
|
function e($s) {
|
|
|
|
|
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
}
|
2024-06-28 05:30:49 +01:00
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Configuration: read DB credentials from environment
|
|
|
|
|
$servername = getenv('MAILSTATS_DB_HOST') ?: '';
|
|
|
|
|
$username = getenv('MAILSTATS_DB_USER') ?: '';
|
|
|
|
|
$password = getenv('MAILSTATS_DB_PASS') ?: '';
|
|
|
|
|
$dbname = getenv('MAILSTATS_DB_NAME') ?: '';
|
|
|
|
|
|
|
|
|
|
// Otherwise try config in /etc/mailstats
|
|
|
|
|
if ($username === '' || $password === '' || $dbname === '') {
|
|
|
|
|
$cfgPath = '/etc/mailstats/db.php';
|
|
|
|
|
if (is_readable($cfgPath)) {
|
|
|
|
|
$cfg = include $cfgPath;
|
|
|
|
|
$servername = $cfg['host'] ?? $servername ?: 'localhost';
|
|
|
|
|
$username = $cfg['user'] ?? $username;
|
|
|
|
|
$password = $cfg['pass'] ?? $password;
|
|
|
|
|
$dbname = $cfg['name'] ?? $dbname;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-28 05:30:49 +01:00
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Fail fast if credentials are not provided via environment
|
|
|
|
|
if ($username === '' || $password === '' || $dbname === '') {
|
|
|
|
|
error_log('Configuration error: DB credentials not set via environment.');
|
|
|
|
|
http_response_code(500);
|
|
|
|
|
exit('Service temporarily unavailable.');
|
|
|
|
|
}
|
2024-06-28 05:30:49 +01:00
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Robust input handling
|
|
|
|
|
$defaultDate = date('Y-m-d', strtotime('-1 day'));
|
|
|
|
|
$date = isset($_GET['date']) ? $_GET['date'] : $defaultDate;
|
|
|
|
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
|
|
|
|
http_response_code(400);
|
|
|
|
|
exit('Invalid date');
|
|
|
|
|
}
|
2024-06-28 05:30:49 +01:00
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// hour: allow 0–23 or special 99 meaning “all hours”
|
|
|
|
|
$hour = isset($_GET['hour']) ? filter_var($_GET['hour'], FILTER_VALIDATE_INT) : 99;
|
|
|
|
|
if ($hour === false || ($hour !== 99 && ($hour < 0 || $hour > 23))) {
|
|
|
|
|
http_response_code(400);
|
|
|
|
|
exit('Invalid hour');
|
2024-06-28 05:30:49 +01:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Pagination
|
|
|
|
|
$page = isset($_GET['page']) ? filter_var($_GET['page'], FILTER_VALIDATE_INT) : 1;
|
|
|
|
|
if ($page === false || $page < 1) { $page = 1; }
|
|
|
|
|
$pageSize = isset($_GET['page_size']) ? filter_var($_GET['page_size'], FILTER_VALIDATE_INT) : 50;
|
|
|
|
|
if ($pageSize === false) { $pageSize = 50; }
|
|
|
|
|
// Bound page size to prevent huge result sets
|
|
|
|
|
if ($pageSize < 1) { $pageSize = 1; }
|
|
|
|
|
if ($pageSize > 100) { $pageSize = 100; }
|
|
|
|
|
$limit = $pageSize;
|
|
|
|
|
$offset = ($page - 1) * $pageSize;
|
|
|
|
|
|
|
|
|
|
// Use mysqli with exceptions and UTF-8
|
|
|
|
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
|
|
|
|
try {
|
|
|
|
|
$conn = new mysqli($servername, $username, $password, $dbname);
|
|
|
|
|
$conn->set_charset('utf8mb4');
|
|
|
|
|
} catch (mysqli_sql_exception $e) {
|
|
|
|
|
error_log('DB connect failed: ' . $e->getMessage());
|
|
|
|
|
http_response_code(500);
|
|
|
|
|
exit('Service temporarily unavailable.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build WHERE clause and bind parameters safely
|
|
|
|
|
$where = 'Date = ?';
|
|
|
|
|
$bindTypesCount = 's';
|
|
|
|
|
$bindValuesCount = [$date];
|
|
|
|
|
|
|
|
|
|
if ($hour !== 99) {
|
|
|
|
|
$where .= ' AND Hour = ?';
|
|
|
|
|
$bindTypesCount .= 'i';
|
|
|
|
|
$bindValuesCount[] = $hour;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Count query for total rows (for display/pagination info)
|
|
|
|
|
try {
|
|
|
|
|
$sqlCount = "SELECT COUNT(*) AS total FROM SummaryLogs WHERE $where";
|
|
|
|
|
$stmtCount = $conn->prepare($sqlCount);
|
|
|
|
|
$stmtCount->bind_param($bindTypesCount, ...$bindValuesCount);
|
|
|
|
|
$stmtCount->execute();
|
|
|
|
|
$resultCount = $stmtCount->get_result();
|
|
|
|
|
$rowCount = $resultCount->fetch_assoc();
|
|
|
|
|
$totalRows = (int)$rowCount['total'];
|
|
|
|
|
$stmtCount->close();
|
|
|
|
|
} catch (mysqli_sql_exception $e) {
|
|
|
|
|
error_log('Count query failed: ' . $e->getMessage());
|
|
|
|
|
http_response_code(500);
|
|
|
|
|
exit('Service temporarily unavailable.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Data query with ORDER and LIMIT/OFFSET
|
|
|
|
|
try {
|
|
|
|
|
$sql = "SELECT id, logData FROM SummaryLogs WHERE $where ORDER BY id DESC LIMIT ? OFFSET ?";
|
|
|
|
|
// Bind types: existing where types + limit (i) + offset (i)
|
|
|
|
|
$bindTypesData = $bindTypesCount . 'ii';
|
|
|
|
|
$bindValuesData = $bindValuesCount;
|
|
|
|
|
$bindValuesData[] = $limit;
|
|
|
|
|
$bindValuesData[] = $offset;
|
|
|
|
|
|
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
|
|
|
$stmt->bind_param($bindTypesData, ...$bindValuesData);
|
|
|
|
|
$stmt->execute();
|
|
|
|
|
$result = $stmt->get_result();
|
|
|
|
|
} catch (mysqli_sql_exception $e) {
|
|
|
|
|
error_log('Data query failed: ' . $e->getMessage());
|
|
|
|
|
http_response_code(500);
|
|
|
|
|
exit('Service temporarily unavailable.');
|
2024-06-30 09:10:57 +01:00
|
|
|
|
}
|
2024-06-28 05:30:49 +01:00
|
|
|
|
|
|
|
|
|
function generateLogDataTable($logData) {
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Defensive decode with substitution for invalid UTF-8
|
|
|
|
|
$data = json_decode($logData, true, 512, JSON_INVALID_UTF8_SUBSTITUTE);
|
|
|
|
|
|
|
|
|
|
if (!is_array($data)) {
|
|
|
|
|
return '<em>Invalid JSON data</em>';
|
2024-06-28 05:30:49 +01:00
|
|
|
|
}
|
2024-06-30 12:05:54 +01:00
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Remove entries with key 'logterse' and entries with empty values
|
2024-06-30 12:05:54 +01:00
|
|
|
|
foreach ($data as $key => $value) {
|
2025-09-02 11:23:48 +01:00
|
|
|
|
if ($key === 'logterse' || $value === '' || $value === null) {
|
2024-06-30 12:05:54 +01:00
|
|
|
|
unset($data[$key]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Merge adjacent duplicates by value
|
2024-06-30 12:05:54 +01:00
|
|
|
|
$mergedData = [];
|
|
|
|
|
$previousValue = null;
|
|
|
|
|
foreach ($data as $key => $value) {
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Normalize non-scalar values for display
|
|
|
|
|
if (is_array($value) || is_object($value)) {
|
|
|
|
|
$value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
|
}
|
|
|
|
|
$valueStr = (string)$value;
|
|
|
|
|
|
|
|
|
|
if ($valueStr === $previousValue) {
|
2024-06-30 12:05:54 +01:00
|
|
|
|
end($mergedData);
|
|
|
|
|
$lastKey = key($mergedData);
|
2025-09-02 11:23:48 +01:00
|
|
|
|
$newKey = $lastKey . '/' . $key;
|
|
|
|
|
$mergedData[$newKey] = $valueStr;
|
2024-06-30 12:05:54 +01:00
|
|
|
|
unset($mergedData[$lastKey]);
|
|
|
|
|
} else {
|
2025-09-02 11:23:48 +01:00
|
|
|
|
$mergedData[$key] = $valueStr;
|
|
|
|
|
}
|
|
|
|
|
$previousValue = $valueStr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional truncation to keep rendering safe
|
|
|
|
|
$maxValueLen = 500;
|
|
|
|
|
foreach ($mergedData as $k => $v) {
|
|
|
|
|
if (mb_strlen($v, 'UTF-8') > $maxValueLen) {
|
|
|
|
|
$mergedData[$k] = mb_substr($v, 0, $maxValueLen, 'UTF-8') . '…';
|
2024-06-30 12:05:54 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$keys = array_keys($mergedData);
|
|
|
|
|
$values = array_values($mergedData);
|
2025-09-02 11:23:48 +01:00
|
|
|
|
|
|
|
|
|
$output = '<table class="stripes" style="border-collapse: collapse; width:95%; overflow-x:auto; margin: 0.6% auto 0.6% auto;"><tbody>';
|
2024-06-28 05:30:49 +01:00
|
|
|
|
|
|
|
|
|
// Divide keys and values into sets of 6
|
2024-06-30 12:05:54 +01:00
|
|
|
|
$chunks = array_chunk($keys, 6);
|
2024-06-28 05:30:49 +01:00
|
|
|
|
foreach ($chunks as $chunkIndex => $chunk) {
|
|
|
|
|
$output .= '<tr>';
|
|
|
|
|
foreach ($chunk as $key) {
|
2025-09-02 11:23:48 +01:00
|
|
|
|
$output .= '<th>' . e($key) . '</th>';
|
2024-06-28 05:30:49 +01:00
|
|
|
|
}
|
|
|
|
|
$output .= '</tr><tr>';
|
|
|
|
|
foreach ($chunk as $i => $key) {
|
2025-09-02 11:23:48 +01:00
|
|
|
|
$val = $values[$chunkIndex * 6 + $i];
|
|
|
|
|
$output .= '<td>' . e($val) . '</td>';
|
2024-06-28 05:30:49 +01:00
|
|
|
|
}
|
|
|
|
|
$output .= '</tr>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$output .= '</tbody></table>';
|
|
|
|
|
return $output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
?>
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<title>Summary Logs</title>
|
2025-09-02 11:23:48 +01:00
|
|
|
|
<link rel="stylesheet" type="text/css" href="css/mailstats.css" />
|
2024-06-28 05:30:49 +01:00
|
|
|
|
</head>
|
|
|
|
|
<body>
|
2025-09-02 11:23:48 +01:00
|
|
|
|
<div style="width:100%; overflow-x:auto; font-size:0.726cqw">
|
|
|
|
|
<h1>
|
|
|
|
|
Summary Logs for Date: <?= e($date) ?>
|
|
|
|
|
<?= $hour === 99 ? ' (All Hours)' : ' at Hour: ' . e($hour) ?>
|
|
|
|
|
</h1>
|
|
|
|
|
<?php
|
|
|
|
|
$startRow = $totalRows > 0 ? ($offset + 1) : 0;
|
|
|
|
|
$endRow = min($offset + $limit, $totalRows);
|
|
|
|
|
?>
|
|
|
|
|
<h3>Found <?= e($totalRows) ?> records. Showing <?= e($startRow) ?>–<?= e($endRow) ?>.</h3>
|
|
|
|
|
|
|
|
|
|
<table style="border-collapse:collapse; width:98%">
|
|
|
|
|
<thead>
|
2024-06-28 05:30:49 +01:00
|
|
|
|
<tr>
|
2025-09-02 11:23:48 +01:00
|
|
|
|
<th>Id</th>
|
|
|
|
|
<th>Details</th>
|
|
|
|
|
<th>Log Data</th>
|
2024-06-28 05:30:49 +01:00
|
|
|
|
</tr>
|
2025-09-02 11:23:48 +01:00
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<?php if ($result && $result->num_rows > 0): ?>
|
|
|
|
|
<?php while ($row = $result->fetch_assoc()): ?>
|
|
|
|
|
<?php
|
|
|
|
|
$id = (int)$row['id'];
|
|
|
|
|
$detailUrl = './ShowDetailedLogs.php?id=' . rawurlencode((string)$id);
|
|
|
|
|
?>
|
|
|
|
|
<tr>
|
|
|
|
|
<td><?= e($id) ?></td>
|
|
|
|
|
<td><a href="<?= e($detailUrl) ?>">View details</a></td>
|
|
|
|
|
<td><?= generateLogDataTable($row['logData']) ?></td>
|
|
|
|
|
</tr>
|
|
|
|
|
<?php endwhile; ?>
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
<tr>
|
|
|
|
|
<td colspan="3">No records found for the specified date and hour.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<?php
|
|
|
|
|
// Simple pagination links
|
|
|
|
|
$baseParams = [
|
|
|
|
|
'date' => $date,
|
|
|
|
|
'hour' => $hour,
|
|
|
|
|
'page_size' => $pageSize
|
|
|
|
|
];
|
|
|
|
|
$prevPage = $page > 1 ? $page - 1 : null;
|
|
|
|
|
$nextPage = ($offset + $limit) < $totalRows ? $page + 1 : null;
|
|
|
|
|
?>
|
|
|
|
|
<div style="margin-top: 1em;">
|
|
|
|
|
<?php if ($prevPage !== null): ?>
|
|
|
|
|
<?php
|
|
|
|
|
$paramsPrev = $baseParams; $paramsPrev['page'] = $prevPage;
|
|
|
|
|
$urlPrev = '?' . http_build_query($paramsPrev, '', '&', PHP_QUERY_RFC3986);
|
|
|
|
|
?>
|
|
|
|
|
<a href="<?= e($urlPrev) ?>">« Previous</a>
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
<?php if ($nextPage !== null): ?>
|
|
|
|
|
<?php
|
|
|
|
|
$paramsNext = $baseParams; $paramsNext['page'] = $nextPage;
|
|
|
|
|
$urlNext = '?' . http_build_query($paramsNext, '', '&', PHP_QUERY_RFC3986);
|
|
|
|
|
?>
|
|
|
|
|
<?php if ($prevPage !== null): ?> | <?php endif; ?>
|
|
|
|
|
<a href="<?= e($urlNext) ?>">Next »</a>
|
2024-06-28 05:30:49 +01:00
|
|
|
|
<?php endif; ?>
|
2025-09-02 11:23:48 +01:00
|
|
|
|
</div>
|
2024-06-30 12:05:54 +01:00
|
|
|
|
</div>
|
2024-06-28 05:30:49 +01:00
|
|
|
|
<?php
|
2025-09-02 11:23:48 +01:00
|
|
|
|
// Clean up
|
|
|
|
|
if (isset($stmt) && $stmt instanceof mysqli_stmt) { $stmt->close(); }
|
|
|
|
|
if (isset($conn) && $conn instanceof mysqli) { $conn->close(); }
|
2024-06-28 05:30:49 +01:00
|
|
|
|
?>
|
|
|
|
|
</body>
|
2025-09-02 11:23:48 +01:00
|
|
|
|
</html>
|