diff --git a/root/etc/mailstats/db.php b/root/etc/mailstats/db.php
new file mode 100644
index 0000000..2752475
--- /dev/null
+++ b/root/etc/mailstats/db.php
@@ -0,0 +1,7 @@
+ 'localhost',
+ 'user' => 'mailstats', //Should be mailstat-ro
+ 'pass' => 'mailstats', //Will be randon strong password
+ 'name' => 'mailstats',
+];
\ No newline at end of file
diff --git a/root/opt/mailstats/html/showSummaryLogs.php b/root/opt/mailstats/html/showSummaryLogs.php
index 9be82d0..061155f 100644
--- a/root/opt/mailstats/html/showSummaryLogs.php
+++ b/root/opt/mailstats/html/showSummaryLogs.php
@@ -1,102 +1,190 @@
connect_error) {
- die("Connection failed: " . $conn->connect_error);
+// 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');
}
-// Prepare and execute the query
-if ($hour == 99){
- $sql = "SELECT * FROM SummaryLogs WHERE Date = ?";
- $stmt = $conn->prepare($sql);
- $stmt->bind_param("s", $date);
-} else {
- $sql = "SELECT * FROM SummaryLogs WHERE Date = ? AND Hour = ?";
- $stmt = $conn->prepare($sql);
- $stmt->bind_param("si", $date, $hour);
+// Helper for safe HTML encoding
+function e($s) {
+ return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+}
+
+// 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;
+ }
+}
+
+// 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.');
+}
+
+// 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');
+}
+
+// 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');
+}
+
+// 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.');
}
-$stmt->execute();
-$result = $stmt->get_result();
-$result_count = $result->num_rows;
function generateLogDataTable($logData) {
- $data = json_decode($logData, true);
- if (is_null($data)) {
- return "Invalid JSON data";
- }
-
- //// Remove entries with the key "logterse"
- //if (isset($data['logterse'])) {
- //unset($data['logterse']);
- //}
+ // Defensive decode with substitution for invalid UTF-8
+ $data = json_decode($logData, true, 512, JSON_INVALID_UTF8_SUBSTITUTE);
- // Remove entries with the key "logterse" and remove entries with empty values
+ if (!is_array($data)) {
+ return 'Invalid JSON data';
+ }
+
+ // Remove entries with key 'logterse' and entries with empty values
foreach ($data as $key => $value) {
- if ($key === 'logterse' || empty($value)) {
+ if ($key === 'logterse' || $value === '' || $value === null) {
unset($data[$key]);
}
}
- // Handle adjacent duplicates by merging keys
+ // Merge adjacent duplicates by value
$mergedData = [];
$previousValue = null;
foreach ($data as $key => $value) {
- if ($value === $previousValue) {
- // Merge the current key with the previous key
+ // 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) {
end($mergedData);
$lastKey = key($mergedData);
- $newKey = "$lastKey/$key";
- $mergedData[$newKey] = $value;
- // Remove the old entry
+ $newKey = $lastKey . '/' . $key;
+ $mergedData[$newKey] = $valueStr;
unset($mergedData[$lastKey]);
} else {
- // Otherwise, add a new entry
- $mergedData[$key] = $value;
+ $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') . '…';
}
- $previousValue = $value;
}
-
$keys = array_keys($mergedData);
$values = array_values($mergedData);
-
- $output = '
';
- #$output = '';
+
+ $output = '';
// Divide keys and values into sets of 6
$chunks = array_chunk($keys, 6);
foreach ($chunks as $chunkIndex => $chunk) {
- if ($chunkIndex > 0) {
- // Add spacing between different sets
- #$output .= ' |
';
- }
-
$output .= '';
foreach ($chunk as $key) {
- $output .= '' . htmlspecialchars($key) . ' | ';
+ $output .= '' . e($key) . ' | ';
}
$output .= '
';
foreach ($chunk as $i => $key) {
- $val = htmlspecialchars($values[$chunkIndex * 6+ $i]);
- if ($key == 'id'){
- $output .= '' . "".$val." | ";
- } else {
- $output .= '' . $val . ' | ';
- }
+ $val = $values[$chunkIndex * 6 + $i];
+ $output .= '' . e($val) . ' | ';
}
$output .= '
';
}
@@ -106,61 +194,87 @@ function generateLogDataTable($logData) {
}
?>
-
-
Summary Logs
-
+
- "
-
Summary Logs for Date: = htmlspecialchars($date) ?> = $hour == 99 ? 'for All Hours' : 'and Hour: ' . htmlspecialchars($hour) ?>
-
Found = $result_count ?> records.
-
-
-
- Id |
-
-
- Log Data |
-
-
-
- num_rows > 0): ?>
- fetch_assoc()): ?>
-
- = htmlspecialchars($row['id']) ?> |
- = generateLogDataTable($row['logData']) ?> |
-
-
-
+
+
+ Summary Logs for Date: = e($date) ?>
+ = $hour === 99 ? ' (All Hours)' : ' at Hour: ' . e($hour) ?>
+
+ 0 ? ($offset + 1) : 0;
+ $endRow = min($offset + $limit, $totalRows);
+ ?>
+
Found = e($totalRows) ?> records. Showing = e($startRow) ?>–= e($endRow) ?>.
+
+
+
- No records found for the specified date and hour. |
+ Id |
+ Details |
+ Log Data |
+
+
+ num_rows > 0): ?>
+ fetch_assoc()): ?>
+
+
+ = e($id) ?> |
+ View details |
+ = generateLogDataTable($row['logData']) ?> |
+
+
+
+
+ No records found for the specified date and hour. |
+
+
+
+
+
+ $date,
+ 'hour' => $hour,
+ 'page_size' => $pageSize
+ ];
+ $prevPage = $page > 1 ? $page - 1 : null;
+ $nextPage = ($offset + $limit) < $totalRows ? $page + 1 : null;
+ ?>
+
-
+
+
+
+ |
+
Next »
+
+
close();
- $conn->close();
+ // Clean up
+ if (isset($stmt) && $stmt instanceof mysqli_stmt) { $stmt->close(); }
+ if (isset($conn) && $conn instanceof mysqli) { $conn->close(); }
?>
-
+