<?php
namespace Modules\Admin\Server\Logs; class ModelGenerateReportLogCounts { public function generate($outputFile, $listingParams) { $model = new Model(); $listingParams->setCurrentPage(0); $listingParams->setResultsPerPage(PHP_INT_MAX); $model->list_execute($listingParams); $listRows = $model->list_getRecordsList(); $fileHandle = fopen($outputFile, 'w'); fwrite($fileHandle, <<<HTML
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>Log Report</title>
	<style>
		body { font-family: monospace; font-size: 12px; }
		.line { padding: 2px; margin: 1px; }
		.count { display: inline-block; width: 40px; text-align: right; padding-right: 10px; font-weight: bold; }
		.addTime { display: inline-block; width: 200px; padding-right: 10px; }
		.id { display: inline-block; width: 40px; padding-right: 10px; }
		.message { display: inline-block; }
		.original-message { display: inline-block;  color: #333; font-weight: bold; margin: 5px 0; }
		.file { padding-left: 300px; color: #666; }
		.stacktrace { padding-left: 300px; color: #999; white-space: pre; }
		.error { background-color: #ffdddd; }
		.warning { background-color: #ffffdd; }
		.notice { background-color: #ddffdd; }
		.tokeniser { color: #888; font-style: italic; }

		/* Collapsible styles */
		.message-group { margin-bottom: 4px; cursor: pointer; }
		.main-row { font-weight: bold; position: relative; }
		.toggle-icon { display: inline-block; width: 10px; margin-right: 5px; }
		.toggle-icon:before { content: '▶'; font-size: 10px; }
		.line.E_USER_NOTICE { border-left: 5px solid #0f0; }
		.line.E_USER_DEPRECATED { border-left: 5px solid #00f; }
		.line.E_STRICT { border-left: 5px solid #0ff; }
		.line.E_RECOVERABLE_ERROR { border-left: 5px solid #f0f; }
		.line.E_CORE_ERROR { border-left: 5px solid #f00; }
		.line.E_CORE_WARNING { border-left: 5px solid #ff0; }
		.line.E_COMPILE_ERROR { border-left: 5px solid #f00; }
		.line.E_COMPILE_WARNING { border-left: 5px solid #ff0; }
		.line.E_PARSE { border-left: 5px solid #f00; }

		/* Collapsible group styles */
		.message-group { margin-bottom: 4px; }
		.message-group .main-row { font-weight: bold; cursor: pointer; position: relative; }
		.message-group .main-row:hover { background-color: rgba(0,0,0,0.05); }
		.message-group .child-rows { display: none; margin-left: 20px; border-left: 2px solid #ccc; padding-left: 10px; cursor: text; }
		.message-group.expanded .main-row { border-bottom: 1px solid #eee; margin-bottom: 5px; }
		.message-group .toggle-icon { display: inline-block; width: 16px; text-align: center; }
		.message-group .toggle-icon:before { content: '▶'; }
		.message-group.expanded .toggle-icon:before { content: '▼'; }
		.message-group .child-row { user-select: text; -webkit-user-select: text; -moz-user-select: text; }

		section.log-header {
			position: fixed;
			width: 100%;
			top: 0;
			left: 0;
			background-color: hsl(180, 25%, 95%);
			border-bottom: 2px solid hsl(180, 25%, 90%);
			z-index: 1;
		}

		.log-header >#stats {
			float: right;
			margin-left: 0.5em;
			padding-left: 0.5em;
			border-left: 1px solid #ccc;
			font-size: 0.75em;
			color: hsl(180, 25%, 33%);
		}

		pre.output {
			margin-top: 4em;
		}
	</style>
</head>
<body>

<section class="log-header">
	<div>
		<button onclick="expandAll()">Expand All</button>
		<button onclick="collapseAll()">Collapse All</button>
		<button onclick="sortByCount()">Sort by Count</button>
		<button onclick="sortByMessage()">Sort by Message</button>
	</div>
	<div id="stats"></div>
	<script>
		// Toggle a specific message group
		function toggleGroup(messageHash) {
			if (event) event.stopPropagation();
			const group = document.querySelector('.message-group[data-hash="' + messageHash + '"]');
			if (group) {
				group.classList.toggle('expanded');
				const childRows = group.querySelector('.child-rows');
				if (childRows) {
					childRows.style.display = group.classList.contains('expanded') ? 'block' : 'none';
				}
			}
		}

		// Expand all message groups
		function expandAll() {
			var groups = document.querySelectorAll('.message-group');
			for (var i = 0; i < groups.length; i++) {
				groups[i].classList.add('expanded');
				var childRows = groups[i].querySelector('.child-rows');
				if (childRows) {
					childRows.style.display = 'block';
				}
			}
		}

		// Collapse all message groups
		function collapseAll() {
			var groups = document.querySelectorAll('.message-group');
			for (var i = 0; i < groups.length; i++) {
				groups[i].classList.remove('expanded');
				var childRows = groups[i].querySelector('.child-rows');
				if (childRows) {
					childRows.style.display = 'none';
				}
			}
		}

		// Sort message groups by count
		function sortByCount() {
			var container = document.querySelector('pre.output');
			if (!container) return;

			var groups = Array.from(document.querySelectorAll('.message-group'));
			groups.sort(function(a, b) {
				var countA = parseInt(a.querySelector('.count').textContent || '0');
				var countB = parseInt(b.querySelector('.count').textContent || '0');
				return countB - countA; // Descending order
			});

			for (var i = 0; i < groups.length; i++) {
				container.appendChild(groups[i]);
			}
		}

		// Sort message groups by message text
		function sortByMessage() {
			var container = document.querySelector('pre.output');
			if (!container) return;

			var groups = Array.from(document.querySelectorAll('.message-group'));
			groups.sort(function(a, b) {
				var messageA = a.querySelector('.message') ? a.querySelector('.message').textContent : '';
				var messageB = b.querySelector('.message') ? b.querySelector('.message').textContent : '';
				return messageA.localeCompare(messageB);
			});

			for (var i = 0; i < groups.length; i++) {
				container.appendChild(groups[i]);
			}
		}

		// Generate statistics
		function generateStats() {
			var groupElements = document.querySelectorAll('.message-group');
			var totalMessages = 0;

			// Count total messages by summing the count in each group
			groupElements.forEach(function(group) {
				var countElement = group.querySelector('.count');
				if (countElement) {
					totalMessages += parseInt(countElement.textContent || '0');
				}
			});

			var statsElement = document.getElementById('stats');
			if (statsElement) {
				statsElement.textContent = 'Showing ' + groupElements.length + ' message groups, totalling ' + totalMessages + ' messages';
			}
		}

		// Initialize when the page loads
		window.onload = function() {
			generateStats();
			// Sort by count by default
			sortByCount();
		};
	</script>
</section>

<pre class="output">
HTML
); $messageGroups = []; foreach ($listRows as $listRow) { $logRow = $model->getFullLogDetails($listRow['id']); if (!$logRow) { continue; } if (!$logRow['context']) { continue; } $normalizedMessage = $this->normalizeMessage($listRow['message']); $messageHash = md5($normalizedMessage); if (!isset($messageGroups[$messageHash])) { $messageGroups[$messageHash] = []; } $messageGroups[$messageHash][] = $logRow; $maxMessageGroups = 10_000_000; if (count($messageGroups) > $maxMessageGroups) { $messageGroups = array_slice($messageGroups, 0, $maxMessageGroups * 0.9, true); } $maxMessageGroupSize = 100; if (count($messageGroups[$messageHash]) > $maxMessageGroupSize) { $messageGroups[$messageHash] = array_slice($messageGroups[$messageHash], 0, $maxMessageGroupSize * 0.9, true); } } uasort($messageGroups, function($a, $b) { return count($b) - count($a); }); $messageGroupsSliced = array_slice($messageGroups, 0, 25, true); if (count(end($messageGroupsSliced)) > 5) { $messageGroupsSliced = array_slice($messageGroups, 0, 50, true); } if (count(end($messageGroupsSliced)) > 5) { $messageGroupsSliced = array_slice($messageGroups, 0, 100, true); } if (count(end($messageGroupsSliced)) > 5) { $messageGroupsSliced = array_slice($messageGroups, 0, 250, true); } $rowIndex = 0; foreach ($messageGroupsSliced as $messageHash => $rows) { $row = $rows[0]; $normalizedMessage = $this->normalizeMessage($row['message']); $colorIndex = strlen($row['message'] ?? '') + strlen($row['context']['$errfile'] ?? '') + strlen(implode('', $row['stacktrace'] ?? [])); $colorIndex = (round($colorIndex/10) * 10) % 360; $fileContent = sprintf( "<div class='message-group' data-hash='%s'>", $messageHash ); $fileContent .= sprintf( "<div style='background-color: hsl(%d, %d%%, %d%%);' class='line main-row %s' onclick='toggleGroup(\"%s\")'>" . "<span class='toggle-icon'></span>" . "<span class='count'>%d</span>" . "<div class='message'>%s</div></div>", $colorIndex, 66, 92, $row['errorLevel'], $messageHash, count($rows), $this->cleanupMessage($normalizedMessage) ); $fileContent .= "<div class='child-rows'>"; foreach ($rows as $childRow) { $fileContent .= sprintf( "<div class='line child-row %s'>" . "<span class='addTime'>%s</span><span class='id'>%s</span>" . "<div class='original-message'>%s</div>" . "<div class='file'>%s</div><div class='stacktrace'>%s</div></div>", $childRow['errorLevel'], $childRow['addTime'], $childRow['id'], $this->cleanupMessage($childRow['message']), $childRow['errfile'], implode("\n", $childRow['stacktrace']) ); } $fileContent .= "</div></div>"; fwrite($fileHandle, $fileContent); $rowIndex++; } fwrite($fileHandle, <<<HTML
</pre>
</body>
</html>
HTML
); fclose($fileHandle); return $this; } protected function cleanupMessage($message) { $message = preg_replace("/^(E_[A-Z]+)/", "<span class='type'>$1</span>", $message); $message = preg_replace("/(TOKENISER_\w+)/", "<span class='tokeniser'>$1</span>", $message); return $message; } protected function normalizeMessage($message) { $message = preg_replace('/\bTINA\b/', '', $message); $preservedPatterns = []; $patternsToPreserve = [ 'module' => '/\[(Modules\\\\[^:]+)::(action[^\]]+)\]/', 'class' => '/\[(\\\\?[A-Za-z0-9_\\\\]+)::([^\]]+)\]/', 'error' => '/\b(E_[A-Z_]+)\b/', 'api' => '/Called ([a-zA-Z0-9_\\]+)\/([^,\s]+)/', 'prefix' => '/\[([A-Z_]+)::[^\]]+\]/', 'exception' => '/Uncaught Exception: ([A-Za-z0-9_\\\\]+)/', ]; foreach ($patternsToPreserve as $type => $pattern) { preg_match_all($pattern, $message, $matches); if (!empty($matches[0])) { foreach ($matches[0] as $i => $fullMatch) { $placeholder = "__TOKENISER_PLACEHOLDER_" . strtoupper($type) . "_" . $i . "_" . md5($i) . "__"; $preservedPatterns[$placeholder] = $fullMatch; $message = str_replace($fullMatch, $placeholder, $message); } } } $patterns = [ '/\b(?:\d{1,3}\.){3}\d{1,3}\b/' => 'TOKENISER_IP_ADDRESS', '/\[SESSION:[^\]]+\]/' => '[SESSION:TOKENISER_xxx]', '/\b(?:\/[\w\.-]+)+:\d+\b/' => 'TOKENISER_FILE_PATH:LINE', '/\b(?:\/[\w\.-]+)+\b/' => 'TOKENISER_FILE_PATH', '/\binvoice(:)? \d+/' => 'invoice TOKENISER_ID', '/\bdocument(:)? \d+/' => 'document TOKENISER_ID', '/\buser(:)? \d+/' => 'user TOKENISER_ID', '/\bid(:)? \d+/' => 'id TOKENISER_ID', '/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i' => 'TOKENISER_UUID', '/\b[0-9a-f]{32}\b/i' => 'TOKENISER_HASH', '/\b\d{4}-\d{2}-\d{2}\b/' => 'TOKENISER_DATE', '/\b\d{2}:\d{2}:\d{2}\b/' => 'TOKENISER_TIME', ]; foreach ($patterns as $pattern => $replacement) { $message = preg_replace($pattern, $replacement, $message); } $tokens = preg_split('/[\s:;.,\/\\\(\)\[\]\{\}\'\"]+/', $message); $normalizedTokens = []; foreach ($tokens as $token) { if (empty($token)) continue; if (is_numeric($token)) { $normalizedTokens[] = 'TOKENISER_NUM'; } elseif (preg_match('/^[0-9a-f]+$/i', $token) && strlen($token) >= 8) { $normalizedTokens[] = 'TOKENISER_HEX'; } elseif (preg_match('/^[\w\.-]+@[\w\.-]+$/', $token)) { $normalizedTokens[] = 'TOKENISER_EMAIL'; } elseif (preg_match('/^[\w\.-]+\.[\w]{2,}$/', $token)) { $normalizedTokens[] = 'TOKENISER_DOMAIN'; } elseif (strlen($token) > 20) { $normalizedTokens[] = sprintf('TOKENISER_LONG_TOKEN_%s', substr(md5($token), 0, 4)); } else { $normalizedTokens[] = $token; } } $message = implode(' ', $normalizedTokens); foreach ($preservedPatterns as $placeholder => $original) { $message = str_replace($placeholder, $original, $message); } return $message; } protected function shouldReportRow(string $messageHash) { return true; } } 