first commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
config.php
|
||||||
|
storage/geoip/*.mmdb
|
||||||
|
storage/geoip/*.bin
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
body {
|
||||||
|
background-color: #f5f6fa;
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 240px;
|
||||||
|
min-height: 100vh;
|
||||||
|
z-index: 1030;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section {
|
||||||
|
padding: 0.5rem 1rem 0.25rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav .nav-link {
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav .nav-link:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav .nav-link.active {
|
||||||
|
color: #fff;
|
||||||
|
border-left: 3px solid #fff;
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
margin-left: 240px;
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.5rem 0.5rem 0 0 !important;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1050;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-map-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > :not(caption) > * > * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.admin-sidebar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
tooltipTriggerList.map(function (el) { return new bootstrap.Tooltip(el); });
|
||||||
|
});
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Genereert een SVG wereldkaart voor de syslog dashboard.
|
||||||
|
* Gebaseerd op Natural Earth data via TopoJSON world-atlas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$url = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
|
||||||
|
$outputFile = __DIR__ . '/../assets/img/world-map.svg';
|
||||||
|
|
||||||
|
echo "Downloaden van wereldkaart data...\n";
|
||||||
|
$json = @file_get_contents($url);
|
||||||
|
if (!$json) {
|
||||||
|
echo "Fout: kan wereldkaart data niet downloaden van {$url}\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
if (!$data || !isset($data['objects']['countries'])) {
|
||||||
|
echo "Fout: ongeldige TopoJSON data\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO numeric -> alpha-2 mapping
|
||||||
|
$isoNumericToAlpha2 = [
|
||||||
|
4 => 'AF', 8 => 'AL', 12 => 'DZ', 20 => 'EG', 24 => 'AO',
|
||||||
|
31 => 'AZ', 32 => 'AR', 36 => 'AU', 40 => 'AT', 50 => 'BD',
|
||||||
|
51 => 'AM', 56 => 'BE', 64 => 'BT', 68 => 'BO', 70 => 'BA',
|
||||||
|
72 => 'BW', 76 => 'BR', 84 => 'BZ', 90 => 'SB', 96 => 'BN',
|
||||||
|
100 => 'BG', 104 => 'MM', 108 => 'BI', 112 => 'BY', 116 => 'KH',
|
||||||
|
120 => 'CM', 124 => 'CA', 132 => 'CV', 140 => 'CF', 144 => 'LK',
|
||||||
|
148 => 'TD', 152 => 'CL', 156 => 'CN', 158 => 'TW', 162 => 'CX',
|
||||||
|
166 => 'CC', 170 => 'CO', 174 => 'KM', 178 => 'CG', 180 => 'CD',
|
||||||
|
184 => 'CK', 188 => 'CR', 191 => 'HR', 192 => 'CU', 196 => 'CY',
|
||||||
|
203 => 'CZ', 204 => 'BJ', 208 => 'DK', 212 => 'DM', 214 => 'DO',
|
||||||
|
218 => 'EC', 222 => 'SV', 226 => 'GQ', 231 => 'ET', 232 => 'ER',
|
||||||
|
233 => 'EE', 234 => 'FO', 238 => 'FK', 239 => 'GS', 242 => 'FJ',
|
||||||
|
246 => 'FI', 248 => 'AX', 250 => 'FR', 254 => 'GF', 258 => 'PF',
|
||||||
|
260 => 'TF', 262 => 'DJ', 266 => 'GA', 268 => 'GE', 270 => 'GM',
|
||||||
|
276 => 'DE', 288 => 'GH', 292 => 'GI', 296 => 'KI', 300 => 'GR',
|
||||||
|
304 => 'GL', 308 => 'GD', 312 => 'GP', 316 => 'GU', 320 => 'GT',
|
||||||
|
324 => 'GN', 328 => 'GY', 332 => 'HT', 334 => 'HM', 336 => 'VA',
|
||||||
|
340 => 'HN', 344 => 'HK', 348 => 'HU', 352 => 'IS', 356 => 'IN',
|
||||||
|
360 => 'ID', 364 => 'IR', 368 => 'IQ', 372 => 'IE', 376 => 'IL',
|
||||||
|
380 => 'IT', 384 => 'CI', 388 => 'JM', 392 => 'JP', 398 => 'KZ',
|
||||||
|
400 => 'JO', 404 => 'KE', 408 => 'KP', 410 => 'KR', 414 => 'KW',
|
||||||
|
417 => 'KG', 418 => 'LA', 422 => 'LB', 426 => 'LS', 428 => 'LV',
|
||||||
|
430 => 'LR', 434 => 'LY', 438 => 'LI', 440 => 'LT', 442 => 'LU',
|
||||||
|
446 => 'MO', 450 => 'MG', 454 => 'MW', 458 => 'MY', 462 => 'MV',
|
||||||
|
466 => 'ML', 470 => 'MT', 478 => 'MR', 480 => 'MU', 484 => 'MX',
|
||||||
|
492 => 'MC', 496 => 'MN', 498 => 'MD', 499 => 'ME', 504 => 'MA',
|
||||||
|
508 => 'MZ', 512 => 'OM', 516 => 'NA', 520 => 'NR', 524 => 'NP',
|
||||||
|
528 => 'NL', 540 => 'NC', 548 => 'VU', 554 => 'NZ', 558 => 'NI',
|
||||||
|
562 => 'NE', 566 => 'NG', 570 => 'NU', 574 => 'NF', 578 => 'NO',
|
||||||
|
580 => 'MP', 583 => 'FM', 584 => 'MH', 585 => 'PW', 586 => 'PK',
|
||||||
|
591 => 'PA', 598 => 'PG', 600 => 'PY', 604 => 'PE', 608 => 'PH',
|
||||||
|
612 => 'PN', 616 => 'PL', 620 => 'PT', 624 => 'GW', 626 => 'TL',
|
||||||
|
630 => 'PR', 634 => 'QA', 634 => 'QA', 638 => 'RE', 642 => 'RO',
|
||||||
|
643 => 'RU', 646 => 'RW', 652 => 'BL', 654 => 'SH', 659 => 'KN',
|
||||||
|
660 => 'AI', 662 => 'LC', 663 => 'MF', 666 => 'PM', 670 => 'VC',
|
||||||
|
674 => 'SM', 678 => 'ST', 682 => 'SA', 686 => 'SN', 688 => 'RS',
|
||||||
|
690 => 'SC', 694 => 'SL', 702 => 'SG', 703 => 'SK', 704 => 'VN',
|
||||||
|
705 => 'SI', 706 => 'SO', 710 => 'ZA', 716 => 'ZW', 724 => 'ES',
|
||||||
|
728 => 'SS', 729 => 'SD', 732 => 'EH', 740 => 'SR', 748 => 'SZ',
|
||||||
|
752 => 'SE', 756 => 'CH', 760 => 'SY', 762 => 'TJ', 764 => 'TH',
|
||||||
|
768 => 'TG', 772 => 'TK', 776 => 'TO', 780 => 'TT', 784 => 'AE',
|
||||||
|
788 => 'TN', 792 => 'TR', 795 => 'TM', 796 => 'TC', 798 => 'TV',
|
||||||
|
800 => 'UG', 804 => 'UA', 807 => 'MK', 818 => 'EG', 826 => 'GB',
|
||||||
|
831 => 'GG', 832 => 'JE', 833 => 'IM', 834 => 'TZ', 840 => 'US',
|
||||||
|
854 => 'BF', 858 => 'UY', 860 => 'UZ', 862 => 'VE', 887 => 'YE',
|
||||||
|
894 => 'ZM',
|
||||||
|
];
|
||||||
|
|
||||||
|
$width = 1000;
|
||||||
|
$height = 400;
|
||||||
|
$t = $data['transform'];
|
||||||
|
$scale = $t['scale'];
|
||||||
|
$translate = $t['translate'];
|
||||||
|
$arcs = $data['arcs'];
|
||||||
|
|
||||||
|
function decodeArc($idx, &$arcs, $scale, $translate) {
|
||||||
|
$reverse = false;
|
||||||
|
if ($idx < 0) {
|
||||||
|
$reverse = true;
|
||||||
|
$idx = -$idx - 1;
|
||||||
|
}
|
||||||
|
$arc = $arcs[$idx];
|
||||||
|
$points = [];
|
||||||
|
$x = 0; $y = 0;
|
||||||
|
foreach ($arc as $delta) {
|
||||||
|
$x += $delta[0];
|
||||||
|
$y += $delta[1];
|
||||||
|
$lon = $x * $scale[0] + $translate[0];
|
||||||
|
$lat = $y * $scale[1] + $translate[1];
|
||||||
|
$points[] = [$lon, $lat];
|
||||||
|
}
|
||||||
|
if ($reverse) $points = array_reverse($points);
|
||||||
|
return $points;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ringToSVG($points, $w, $h) {
|
||||||
|
if (count($points) < 3) return '';
|
||||||
|
$parts = [];
|
||||||
|
foreach ($points as $i => $p) {
|
||||||
|
$px = ($p[0] + 180) / 360 * $w;
|
||||||
|
$py = (90 - $p[1]) / 180 * $h;
|
||||||
|
$parts[] = ($i === 0 ? 'M' : 'L') . round($px, 2) . ',' . round($py, 2);
|
||||||
|
}
|
||||||
|
$parts[] = 'Z';
|
||||||
|
return implode(' ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
$svgPaths = [];
|
||||||
|
|
||||||
|
foreach ($data['objects']['countries']['geometries'] as $geom) {
|
||||||
|
$numId = (int)$geom['id'];
|
||||||
|
$isoAlpha2 = $isoNumericToAlpha2[$numId] ?? '';
|
||||||
|
$name = $geom['properties']['name'] ?? 'Unknown';
|
||||||
|
if ($isoAlpha2 === 'AQ') continue;
|
||||||
|
|
||||||
|
$polygons = [];
|
||||||
|
if ($geom['type'] === 'Polygon') {
|
||||||
|
$polygons[] = $geom['arcs'];
|
||||||
|
} elseif ($geom['type'] === 'MultiPolygon') {
|
||||||
|
$polygons = $geom['arcs'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($polygons as $rings) {
|
||||||
|
$allPoints = [];
|
||||||
|
foreach ($rings as $ring) {
|
||||||
|
foreach ($ring as $arcIdx) {
|
||||||
|
$pts = decodeArc($arcIdx, $arcs, $scale, $translate);
|
||||||
|
$allPoints = array_merge($allPoints, $pts);
|
||||||
|
}
|
||||||
|
break; // outer ring only, skip holes
|
||||||
|
}
|
||||||
|
$d = ringToSVG($allPoints, $width, $height);
|
||||||
|
if ($d) {
|
||||||
|
$svgPaths[] = ['iso' => $isoAlpha2, 'name' => $name, 'd' => $d];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $width . ' ' . $height . '" class="codepress-world-map" width="100%" height="100%">
|
||||||
|
<style>
|
||||||
|
.codepress-world-map { display: block; background: #eaf2fb; }
|
||||||
|
.codepress-world-map .country { fill: #dfe4ea; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: round; vector-effect: non-scaling-stroke; transition: fill .15s ease; }
|
||||||
|
.codepress-world-map .country:hover { fill: #ffc107; }
|
||||||
|
</style>
|
||||||
|
<g class="countries">';
|
||||||
|
|
||||||
|
foreach ($svgPaths as $c) {
|
||||||
|
$svg .= "\n" . ' <path class="country" id="' . htmlspecialchars($c['iso']) . '" data-name="' . htmlspecialchars($c['name']) . '" d="' . $c['d'] . '" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
$svg .= '
|
||||||
|
</g>
|
||||||
|
</svg>';
|
||||||
|
|
||||||
|
file_put_contents($outputFile, $svg);
|
||||||
|
echo "Wereldkaart opgeslagen: {$outputFile}\n";
|
||||||
|
echo count($svgPaths) . " landpaden gegenereerd.\n";
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Download de DB-IP Country Lite database voor GeoIP lookups.
|
||||||
|
* Dit is een gratis database met wekelijkse updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$outputDir = __DIR__ . '/../storage/geoip';
|
||||||
|
|
||||||
|
if (!is_dir($outputDir)) {
|
||||||
|
mkdir($outputDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$yearMonth = date('Y-m');
|
||||||
|
$url = "https://download.db-ip.com/free/dbip-country-lite-{$yearMonth}.csv.gz";
|
||||||
|
$gzFile = $outputDir . '/dbip-country-lite.csv.gz';
|
||||||
|
$csvFile = $outputDir . '/dbip-country-lite.csv';
|
||||||
|
|
||||||
|
echo "Downloaden GeoIP database van {$url}...\n";
|
||||||
|
|
||||||
|
$fp = fopen($gzFile, 'w');
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_FILE => $fp,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_TIMEOUT => 120,
|
||||||
|
CURLOPT_USERAGENT => 'SyslogDash/1.0',
|
||||||
|
]);
|
||||||
|
curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
fclose($fp);
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
unlink($gzFile);
|
||||||
|
echo "Fout: HTTP {$httpCode} bij downloaden GeoIP database\n";
|
||||||
|
echo "Tip: bezoek https://www.db-ip.com/db/lite.php voor handmatige download\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Uitpakken...\n";
|
||||||
|
$gz = gzopen($gzFile, 'r');
|
||||||
|
$csv = fopen($csvFile, 'w');
|
||||||
|
while ($line = gzgets($gz)) {
|
||||||
|
fputs($csv, $line);
|
||||||
|
}
|
||||||
|
gzclose($gz);
|
||||||
|
fclose($csv);
|
||||||
|
unlink($gzFile);
|
||||||
|
|
||||||
|
// Convert to binary format for faster lookups
|
||||||
|
$ipv4File = $outputDir . '/ipv4.bin';
|
||||||
|
$ipv6File = $outputDir . '/ipv6.bin';
|
||||||
|
|
||||||
|
$fh4 = fopen($ipv4File, 'w');
|
||||||
|
$fh6 = fopen($ipv6File, 'w');
|
||||||
|
$count4 = 0;
|
||||||
|
$count6 = 0;
|
||||||
|
|
||||||
|
$csv = fopen($csvFile, 'r');
|
||||||
|
while (($row = fgetcsv($csv)) !== false) {
|
||||||
|
if (count($row) < 3) continue;
|
||||||
|
$start = trim($row[0]);
|
||||||
|
$end = trim($row[1]);
|
||||||
|
$code = trim($row[2]);
|
||||||
|
if (strlen($code) !== 2) continue;
|
||||||
|
|
||||||
|
if (filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$startLong = ip2long($start);
|
||||||
|
$endLong = ip2long($end);
|
||||||
|
if ($startLong === false || $endLong === false) continue;
|
||||||
|
fwrite($fh4, pack('NNa2', $startLong, $endLong, $code));
|
||||||
|
$count4++;
|
||||||
|
} elseif (filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$startBin = inet_pton($start);
|
||||||
|
$endBin = inet_pton($end);
|
||||||
|
if ($startBin === false || $endBin === false) continue;
|
||||||
|
fwrite($fh6, $startBin . $endBin . $code);
|
||||||
|
$count6++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($csv);
|
||||||
|
fclose($fh4);
|
||||||
|
fclose($fh6);
|
||||||
|
|
||||||
|
unlink($csvFile);
|
||||||
|
|
||||||
|
echo "GeoIP database bijgewerkt:\n";
|
||||||
|
echo " IPv4: {$count4} ranges\n";
|
||||||
|
echo " IPv6: {$count6} ranges\n";
|
||||||
|
echo " Bestanden: {$ipv4File}, {$ipv6File}\n";
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "noorlander/syslogdash",
|
||||||
|
"description": "Syslog Dashboard met GeoIP wereldkaart en BI mogelijkheden",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-json": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"SyslogDash\\": "src/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+23
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "0a0f92517e549816293da40320cffa7e",
|
||||||
|
"packages": [],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-json": "*"
|
||||||
|
},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.9.0"
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'db' => [
|
||||||
|
'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=Syslog;charset=utf8mb4',
|
||||||
|
'username' => 'syslog',
|
||||||
|
'password' => '',
|
||||||
|
],
|
||||||
|
'geoip' => [
|
||||||
|
'provider' => 'maxmind',
|
||||||
|
'maxmind_db' => __DIR__ . '/storage/geoip/GeoLite2-Country.mmdb',
|
||||||
|
'dbip_dir' => __DIR__ . '/storage/geoip',
|
||||||
|
],
|
||||||
|
'syslog' => [
|
||||||
|
'table' => 'SystemEvents',
|
||||||
|
'host_to_ip' => [
|
||||||
|
// 'hostname' => '1.2.3.4',
|
||||||
|
],
|
||||||
|
'resolve_hostnames' => true,
|
||||||
|
],
|
||||||
|
'dashboard' => [
|
||||||
|
'title' => 'Syslog Dash',
|
||||||
|
'theme_color' => '#0a369d',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use SyslogDash\Layout;
|
||||||
|
|
||||||
|
$page = $_GET['page'] ?? 'dashboard';
|
||||||
|
$allowed = ['dashboard', 'worldmap', 'logs', 'bi', 'export'];
|
||||||
|
|
||||||
|
if (!in_array($page, $allowed)) {
|
||||||
|
$page = 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($page === 'export') {
|
||||||
|
require __DIR__ . '/pages/export.php';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout::render($page);
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
<?php
|
||||||
|
use SyslogDash\SyslogQuery;
|
||||||
|
use SyslogDash\GeoIP;
|
||||||
|
|
||||||
|
$sq = new SyslogQuery();
|
||||||
|
|
||||||
|
$period = (int)($_GET['period'] ?? 30);
|
||||||
|
if (!in_array($period, [7, 30, 90, 365, 0])) $period = 30;
|
||||||
|
|
||||||
|
$report = $_GET['report'] ?? 'daily';
|
||||||
|
$allowedReports = ['daily', 'hosts', 'countries', 'severity', 'tags', 'hourly', 'custom'];
|
||||||
|
if (!in_array($report, $allowedReports)) $report = 'daily';
|
||||||
|
|
||||||
|
$chartData = [];
|
||||||
|
$chartType = 'bar';
|
||||||
|
$chartLabels = [];
|
||||||
|
$chartValues = [];
|
||||||
|
$chartLabel = '';
|
||||||
|
$extraRows = [];
|
||||||
|
|
||||||
|
switch ($report) {
|
||||||
|
case 'daily':
|
||||||
|
$chartData = $sq->getDailyStats($period);
|
||||||
|
$chartLabels = array_map(fn($d) => date('d-m', strtotime($d['date'])), $chartData);
|
||||||
|
$chartValues = array_column($chartData, 'cnt');
|
||||||
|
$chartLabel = 'Berichten per dag';
|
||||||
|
$chartType = 'bar';
|
||||||
|
$extraRows = $chartData;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'hosts':
|
||||||
|
$chartData = $sq->getHostStats($period);
|
||||||
|
$chartLabels = array_column($chartData, 'FromHost');
|
||||||
|
$chartValues = array_column($chartData, 'cnt');
|
||||||
|
$chartLabel = 'Top hosts';
|
||||||
|
$chartType = 'bar';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'countries':
|
||||||
|
$chartData = $sq->getCountryStats($period);
|
||||||
|
$chartLabels = array_map(fn($c) => GeoIP::getCountryName($c['code']) . ' (' . $c['code'] . ')', $chartData);
|
||||||
|
$chartValues = array_column($chartData, 'count');
|
||||||
|
$chartLabel = 'Berichten per land';
|
||||||
|
$chartType = 'bar';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'severity':
|
||||||
|
$chartData = $sq->getSeverityStats($period);
|
||||||
|
$chartLabels = array_map(fn($s) => SyslogQuery::severityName((int)$s['Priority']), $chartData);
|
||||||
|
$chartValues = array_column($chartData, 'cnt');
|
||||||
|
$chartLabel = 'Severity verdeling';
|
||||||
|
$chartType = 'doughnut';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tags':
|
||||||
|
$chartData = $sq->getTagStats($period);
|
||||||
|
$chartLabels = array_column($chartData, 'SysLogTag');
|
||||||
|
$chartValues = array_column($chartData, 'cnt');
|
||||||
|
$chartLabel = 'Top tags';
|
||||||
|
$chartType = 'bar';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'hourly':
|
||||||
|
$db = \SyslogDash\Database::getInstance();
|
||||||
|
$table = Database::table();
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT HOUR(`ReceivedAt`) AS h, COUNT(*) AS cnt
|
||||||
|
FROM `{$table}`
|
||||||
|
WHERE `ReceivedAt` >= :since
|
||||||
|
GROUP BY HOUR(`ReceivedAt`)
|
||||||
|
ORDER BY h ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([':since' => date('Y-m-d H:i:s', strtotime("-{$period} days"))]);
|
||||||
|
$chartData = $stmt->fetchAll();
|
||||||
|
$hourlyData = array_fill(0, 24, 0);
|
||||||
|
foreach ($chartData as $r) $hourlyData[(int)$r['h']] = (int)$r['cnt'];
|
||||||
|
$chartLabels = array_map(fn($h) => sprintf('%02d:00', $h), range(0, 23));
|
||||||
|
$chartValues = $hourlyData;
|
||||||
|
$chartLabel = 'Berichten per uur';
|
||||||
|
$chartType = 'line';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'custom':
|
||||||
|
$sql = $_POST['sql'] ?? '';
|
||||||
|
$customResult = null;
|
||||||
|
if ($sql && preg_match('/^\s*SELECT/i', $sql)) {
|
||||||
|
$customResult = $sq->runQuery($sql);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-bar-chart-line me-2"></i>BI Rapporten</h2>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="?page=bi&period=7&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 7 ? 'active' : '' ?>">7d</a>
|
||||||
|
<a href="?page=bi&period=30&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 30 ? 'active' : '' ?>">30d</a>
|
||||||
|
<a href="?page=bi&period=90&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 90 ? 'active' : '' ?>">90d</a>
|
||||||
|
<a href="?page=bi&period=365&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 365 ? 'active' : '' ?>">365d</a>
|
||||||
|
<a href="?page=bi&period=0&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 0 ? 'active' : '' ?>">Alles</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="list-group list-group-sm">
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=daily" class="list-group-item list-group-item-action py-2 <?= $report === 'daily' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-calendar me-2"></i>Dagelijks
|
||||||
|
</a>
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=hourly" class="list-group-item list-group-item-action py-2 <?= $report === 'hourly' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-clock me-2"></i>Per uur
|
||||||
|
</a>
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=hosts" class="list-group-item list-group-item-action py-2 <?= $report === 'hosts' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-hdd-network me-2"></i>Top hosts
|
||||||
|
</a>
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=countries" class="list-group-item list-group-item-action py-2 <?= $report === 'countries' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-globe me-2"></i>Landen
|
||||||
|
</a>
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=severity" class="list-group-item list-group-item-action py-2 <?= $report === 'severity' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>Severity
|
||||||
|
</a>
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=tags" class="list-group-item list-group-item-action py-2 <?= $report === 'tags' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-tags me-2"></i>Tags
|
||||||
|
</a>
|
||||||
|
<a href="?page=bi&period=<?= $period ?>&report=custom" class="list-group-item list-group-item-action py-2 <?= $report === 'custom' ? 'active' : '' ?>">
|
||||||
|
<i class="bi bi-code-slash me-2"></i>Custom SQL
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<?php if ($report === 'custom'): ?>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-code-slash me-2"></i>Custom SQL Query</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="?page=bi&period=<?= $period ?>&report=custom">
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea name="sql" class="form-control font-monospace" rows="4" placeholder="SELECT COUNT(*), FromHost FROM SystemEvents GROUP BY FromHost ORDER BY 1 DESC LIMIT 10"><?= htmlspecialchars($_POST['sql'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="bi bi-play me-1"></i>Uitvoeren</button>
|
||||||
|
</form>
|
||||||
|
<?php if (isset($customResult)): ?>
|
||||||
|
<?php if ($customResult['error']): ?>
|
||||||
|
<div class="alert alert-danger mt-3"><?= htmlspecialchars($customResult['error']) ?></div>
|
||||||
|
<?php elseif (!empty($customResult['rows'])): ?>
|
||||||
|
<div class="table-responsive mt-3" style="max-height:500px">
|
||||||
|
<table class="table table-sm table-hover table-bordered small mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<?php foreach (array_keys($customResult['rows'][0]) as $col): ?>
|
||||||
|
<th><?= htmlspecialchars($col) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($customResult['rows'] as $r): ?>
|
||||||
|
<tr>
|
||||||
|
<?php foreach ($r as $v): ?>
|
||||||
|
<td><?= htmlspecialchars(mb_substr((string)$v, 0, 200)) ?></td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted mt-2 d-block"><?= count($customResult['rows']) ?> rijen</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-graph-up me-2"></i><?= $chartLabel ?></span>
|
||||||
|
<div>
|
||||||
|
<a href="?page=export&report=<?= $report ?>&period=<?= $period ?>" class="btn btn-sm btn-outline-success"><i class="bi bi-download"></i> CSV</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="biChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($report === 'daily' && !empty($extraRows)): ?>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-table me-2"></i>Dagelijkse gegevens</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive" style="max-height:400px">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr><th>Datum</th><th>Berichten</th><th>Hosts</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($extraRows as $r): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= date('d-m-Y', strtotime($r['date'])) ?></td>
|
||||||
|
<td><?= number_format((int)$r['cnt'], 0, ',', '.') ?></td>
|
||||||
|
<td><?= $r['hosts'] ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($report !== 'custom' && !empty($chartLabels)): ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var ctx = document.getElementById('biChart');
|
||||||
|
if (!ctx) return;
|
||||||
|
var config = {
|
||||||
|
type: '<?= $chartType ?>',
|
||||||
|
data: {
|
||||||
|
labels: <?= json_encode($chartLabels) ?>,
|
||||||
|
datasets: [{
|
||||||
|
label: '<?= $chartLabel ?>',
|
||||||
|
data: <?= json_encode($chartValues) ?>,
|
||||||
|
backgroundColor: <?= $chartType === 'doughnut'
|
||||||
|
? "['#dc3545','#ffc107','#0dcaf0','#198754','#6f42c1','#fd7e14','#20c997','#0d6efd']"
|
||||||
|
: "'#0d6efd'" ?>,
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
|
||||||
|
scales: <?= $chartType !== 'doughnut' ? "{ y: { beginAtZero: true } }" : "{}" ?>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Chart(ctx, config);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php $sq = null; ?>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
use SyslogDash\SyslogQuery;
|
||||||
|
use SyslogDash\GeoIP;
|
||||||
|
|
||||||
|
$sq = new SyslogQuery();
|
||||||
|
$kpi = $sq->getKPIStats(30);
|
||||||
|
$daily = $sq->getDailyStats(30);
|
||||||
|
$hostStats = $sq->getHostStats(30);
|
||||||
|
$sevStats = $sq->getSeverityStats(30);
|
||||||
|
$tagStats = $sq->getTagStats(30);
|
||||||
|
$topCountry = $kpi['topCountry'];
|
||||||
|
?>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Dashboard</h2>
|
||||||
|
<span class="text-muted small">Laatste 30 dagen</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-primary-subtle text-primary"><i class="bi bi-envelope"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= number_format($kpi['total'], 0, ',', '.') ?></div>
|
||||||
|
<div class="stat-label">Log berichten</div>
|
||||||
|
<small class="text-muted"><?= number_format($kpi['avgPerDay'], 0, ',', '.') ?>/dag</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-success-subtle text-success"><i class="bi bi-hdd-network"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= $kpi['uniqueHosts'] ?></div>
|
||||||
|
<div class="stat-label">Unieke hosts</div>
|
||||||
|
<small class="text-muted">apparaten</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-info-subtle text-info"><i class="bi bi-globe"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?php if ($topCountry): ?><?= GeoIP::getCountryFlagEmoji($topCountry['code']) ?> <?= GeoIP::getCountryName($topCountry['code']) ?><?php else: ?>—<?php endif; ?></div>
|
||||||
|
<div class="stat-label">Grootste land</div>
|
||||||
|
<small class="text-muted"><?= $topCountry ? number_format($topCountry['count'], 0, ',', '.') . ' berichten' : '' ?></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-warning-subtle text-warning"><i class="bi bi-clock-history"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= number_format($kpi['totalAll'], 0, ',', '.') ?></div>
|
||||||
|
<div class="stat-label">Totaal (all-time)</div>
|
||||||
|
<small class="text-muted">sinds begin</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-graph-up me-2"></i>Berichten per dag</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="dailyChart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-pie-chart me-2"></i>Severity</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="sevChart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-hdd-network me-2"></i>Top hosts</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<?php $maxHost = max(array_column($hostStats, 'cnt')); ?>
|
||||||
|
<?php foreach ($hostStats as $h): ?>
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<a href="?page=logs&host=<?= urlencode($h['FromHost']) ?>" class="text-decoration-none"><?= htmlspecialchars($h['FromHost']) ?></a>
|
||||||
|
<span class="badge bg-primary rounded-pill"><?= number_format($h['cnt'], 0, ',', '.') ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height:4px">
|
||||||
|
<div class="progress-bar" style="width:<?= $maxHost > 0 ? ($h['cnt']/$maxHost*100) : 0 ?>%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-tags me-2"></i>Top tags</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<?php $maxTag = max(array_column($tagStats, 'cnt')); ?>
|
||||||
|
<?php foreach ($tagStats as $t): ?>
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="text-muted small font-monospace"><?= htmlspecialchars($t['SysLogTag'] ?? '—') ?></span>
|
||||||
|
<span class="badge bg-secondary rounded-pill"><?= number_format($t['cnt'], 0, ',', '.') ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height:4px">
|
||||||
|
<div class="progress-bar bg-info" style="width:<?= $maxTag > 0 ? ($t['cnt']/$maxTag*100) : 0 ?>%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
new Chart(document.getElementById('dailyChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: <?= json_encode(array_map(fn($d) => date('d-m', strtotime($d['date'])), $daily)) ?>,
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Berichten', data: <?= json_encode(array_column($daily, 'cnt')) ?>, backgroundColor: '#0d6efd', borderRadius: 2 },
|
||||||
|
{ label: 'Hosts', data: <?= json_encode(array_column($daily, 'hosts')) ?>, backgroundColor: '#6ea8fe', borderRadius: 2 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
|
||||||
|
scales: { y: { beginAtZero: true } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('sevChart'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: <?= json_encode(array_map(fn($s) => SyslogQuery::severityName((int)$s['Priority']), $sevStats)) ?>,
|
||||||
|
datasets: [{
|
||||||
|
data: <?= json_encode(array_column($sevStats, 'cnt')) ?>,
|
||||||
|
backgroundColor: ['#dc3545','#dc3545','#dc3545','#ffc107','#ffc107','#0dcaf0','#6c757d','#6c757d']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { position: 'right', labels: { boxWidth: 12, font: { size: 11 } } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php $sq = null; // close PDO connection ?>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use SyslogDash\SyslogQuery;
|
||||||
|
use SyslogDash\GeoIP;
|
||||||
|
|
||||||
|
$sq = new SyslogQuery();
|
||||||
|
$format = $_GET['format'] ?? 'csv';
|
||||||
|
$period = (int)($_GET['period'] ?? 30);
|
||||||
|
if (!in_array($period, [7, 30, 90, 365, 0])) $period = 30;
|
||||||
|
$report = $_GET['report'] ?? 'logs';
|
||||||
|
|
||||||
|
$filename = 'syslog-export-' . date('Ymd') . '.' . $format;
|
||||||
|
|
||||||
|
switch ($report) {
|
||||||
|
case 'logs':
|
||||||
|
$filters = [];
|
||||||
|
foreach (['host','country','tag','facility','priority','search','from','to'] as $k) {
|
||||||
|
if (!empty($_GET[$k])) $filters[$k] = $_GET[$k];
|
||||||
|
}
|
||||||
|
$filters['sort'] = 'ASC';
|
||||||
|
$result = $sq->getLogs($filters, 1, 100000);
|
||||||
|
$data = $result['rows'];
|
||||||
|
$fields = ['ID','ReceivedAt','FromHost','Facility','Priority','SysLogTag','Message'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'daily':
|
||||||
|
$data = $sq->getDailyStats($period);
|
||||||
|
$fields = ['Datum','Berichten','Hosts'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'hosts':
|
||||||
|
$data = $sq->getHostStats($period);
|
||||||
|
$fields = ['Host','Aantal'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'countries':
|
||||||
|
$data = $sq->getCountryStats($period);
|
||||||
|
$fields = ['Code','Aantal'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'severity':
|
||||||
|
$data = $sq->getSeverityStats($period);
|
||||||
|
$fields = ['Priority','Severity','Aantal'];
|
||||||
|
array_walk($data, fn(&$r) => $r['SeverityName'] = SyslogQuery::severityName((int)$r['Priority']));
|
||||||
|
$fields = ['Priority','SeverityName','Aantal'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$data = [];
|
||||||
|
$fields = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($format === 'json') {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header("Content-Disposition: attachment; filename=\"{$filename}\"");
|
||||||
|
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header("Content-Disposition: attachment; filename=\"{$filename}\"");
|
||||||
|
echo "\xEF\xBB\xBF"; // BOM for Excel UTF-8
|
||||||
|
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
fputcsv($out, $fields, ';');
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$line = [];
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$line[] = $row[$f] ?? '';
|
||||||
|
}
|
||||||
|
fputcsv($out, $line, ';');
|
||||||
|
}
|
||||||
|
fclose($out);
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
use SyslogDash\SyslogQuery;
|
||||||
|
|
||||||
|
$sq = new SyslogQuery();
|
||||||
|
|
||||||
|
$filters = [
|
||||||
|
'host' => $_GET['host'] ?? '',
|
||||||
|
'country' => $_GET['country'] ?? '',
|
||||||
|
'tag' => $_GET['tag'] ?? '',
|
||||||
|
'facility' => $_GET['facility'] ?? '',
|
||||||
|
'priority' => $_GET['priority'] ?? '',
|
||||||
|
'search' => $_GET['search'] ?? '',
|
||||||
|
'from' => $_GET['from'] ?? '',
|
||||||
|
'to' => $_GET['to'] ?? '',
|
||||||
|
'sort' => $_GET['sort'] ?? 'DESC',
|
||||||
|
'sort_col' => $_GET['sort_col'] ?? 'ReceivedAt',
|
||||||
|
];
|
||||||
|
$filters = array_filter($filters, fn($v) => $v !== '');
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
|
||||||
|
$result = $sq->getLogs($filters, $page, 50);
|
||||||
|
$hosts = $sq->getDistinct('FromHost');
|
||||||
|
$facilities = $sq->getDistinct('Facility');
|
||||||
|
$priorities = $sq->getDistinct('Priority');
|
||||||
|
|
||||||
|
function sortUrl($col, $filters, $currentSort, $currentCol) {
|
||||||
|
$dir = 'ASC';
|
||||||
|
if ($currentCol === $col && $currentSort === 'ASC') $dir = 'DESC';
|
||||||
|
$params = array_merge($filters, ['sort_col' => $col, 'sort' => $dir, 'p' => 1]);
|
||||||
|
return '?page=logs&' . http_build_query($params);
|
||||||
|
}
|
||||||
|
function filterUrl($filters, $override) {
|
||||||
|
$params = array_merge($filters, $override, ['p' => 1]);
|
||||||
|
return '?page=logs&' . http_build_query($params);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-list-ul me-2"></i>Logs</h2>
|
||||||
|
<div>
|
||||||
|
<a href="?page=export&<?= http_build_query($_GET) ?>" class="btn btn-sm btn-outline-success me-1"><i class="bi bi-download"></i> CSV</a>
|
||||||
|
<a href="?page=export&format=json&<?= http_build_query($_GET) ?>" class="btn btn-sm btn-outline-info"><i class="bi bi-download"></i> JSON</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-funnel me-2"></i>Filters</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" action="" class="row g-3">
|
||||||
|
<input type="hidden" name="page" value="logs">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">Host</label>
|
||||||
|
<select name="host" class="form-select form-select-sm">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<?php foreach ($hosts as $h): ?>
|
||||||
|
<option value="<?= htmlspecialchars($h) ?>" <?= ($filters['host'] ?? '') === $h ? 'selected' : '' ?>><?= htmlspecialchars($h) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">Facility</label>
|
||||||
|
<select name="facility" class="form-select form-select-sm">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<?php foreach ($facilities as $f): ?>
|
||||||
|
<option value="<?= $f ?>" <?= ($filters['facility'] ?? '') === (string)$f ? 'selected' : '' ?>><?= SyslogQuery::facilityName((int)$f) ?> (<?= $f ?>)</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">Severity</label>
|
||||||
|
<select name="priority" class="form-select form-select-sm">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<?php foreach ($priorities as $p): ?>
|
||||||
|
<option value="<?= $p ?>" <?= ($filters['priority'] ?? '') === (string)$p ? 'selected' : '' ?>><?= SyslogQuery::severityName((int)$p) ?> (<?= $p ?>)</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">Vanaf</label>
|
||||||
|
<input type="date" name="from" class="form-control form-control-sm" value="<?= htmlspecialchars($filters['from'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">Tot</label>
|
||||||
|
<input type="date" name="to" class="form-control form-control-sm" value="<?= htmlspecialchars($filters['to'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">Zoeken</label>
|
||||||
|
<input type="text" name="search" class="form-control form-control-sm" placeholder="zoek in bericht..." value="<?= htmlspecialchars($filters['search'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search me-1"></i>Filteren</button>
|
||||||
|
<a href="?page=logs" class="btn btn-outline-secondary btn-sm"><i class="bi bi-x"></i>Wissen</a>
|
||||||
|
<small class="text-muted ms-2"><?= number_format($result['total'], 0, ',', '.') ?> resultaten (pagina <?= $result['page'] ?>/<?= $result['pages'] ?>)</small>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0" style="font-size:0.85rem;">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th><a href="<?= sortUrl('ReceivedAt', $filters, $filters['sort'] ?? 'DESC', $filters['sort_col'] ?? 'ReceivedAt') ?>" class="text-decoration-none text-dark">Ontvangen <?= ($filters['sort_col'] ?? 'ReceivedAt') === 'ReceivedAt' ? (($filters['sort'] ?? 'DESC') === 'ASC' ? '↑' : '↓') : '' ?></a></th>
|
||||||
|
<th><a href="<?= sortUrl('FromHost', $filters, $filters['sort'] ?? 'DESC', $filters['sort_col'] ?? 'ReceivedAt') ?>" class="text-decoration-none text-dark">Host <?= ($filters['sort_col'] ?? '') === 'FromHost' ? (($filters['sort'] ?? 'DESC') === 'ASC' ? '↑' : '↓') : '' ?></a></th>
|
||||||
|
<th>Facility</th>
|
||||||
|
<th>Severity</th>
|
||||||
|
<th>Tag</th>
|
||||||
|
<th>Bericht</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($result['rows'] as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap small"><?= date('d-m-Y H:i:s', strtotime($row['ReceivedAt'])) ?></td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
<a href="?page=logs&host=<?= urlencode($row['FromHost']) ?>" class="text-decoration-none"><?= htmlspecialchars($row['FromHost']) ?></a>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge bg-light text-dark"><?= SyslogQuery::facilityName((int)$row['Facility']) ?></span></td>
|
||||||
|
<td><span class="badge bg-<?= SyslogQuery::severityClass((int)$row['Priority']) ?>"><?= SyslogQuery::severityName((int)$row['Priority']) ?></span></td>
|
||||||
|
<td class="font-monospace small text-muted"><?= htmlspecialchars($row['SysLogTag'] ?? '') ?></td>
|
||||||
|
<td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="<?= htmlspecialchars($row['Message']) ?>"><?= htmlspecialchars(mb_substr($row['Message'] ?? '', 0, 120)) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($result['pages'] > 1): ?>
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center">
|
||||||
|
<li class="page-item <?= $result['page'] <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="<?= filterUrl($filters, ['p' => $result['page'] - 1]) ?>">Vorige</a>
|
||||||
|
</li>
|
||||||
|
<?php for ($i = 1; $i <= $result['pages']; $i++): ?>
|
||||||
|
<li class="page-item <?= $i === $result['page'] ? 'active' : '' ?>">
|
||||||
|
<a class="page-link" href="<?= filterUrl($filters, ['p' => $i]) ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $result['page'] >= $result['pages'] ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="<?= filterUrl($filters, ['p' => $result['page'] + 1]) ?>">Volgende</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php $sq = null; ?>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
use SyslogDash\SyslogQuery;
|
||||||
|
use SyslogDash\GeoIP;
|
||||||
|
|
||||||
|
$period = (int)($_GET['period'] ?? 30);
|
||||||
|
if (!in_array($period, [7, 30, 90, 365, 0])) $period = 30;
|
||||||
|
|
||||||
|
$sq = new SyslogQuery();
|
||||||
|
$countryStats = $sq->getCountryStats($period);
|
||||||
|
$daily = $sq->getDailyStats($period);
|
||||||
|
$totalLogs = array_sum(array_column($countryStats, 'count'));
|
||||||
|
|
||||||
|
$mapCountries = [];
|
||||||
|
foreach ($countryStats as $c) {
|
||||||
|
$mapCountries[$c['code']] = $c['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$drillCountry = $_GET['country'] ?? null;
|
||||||
|
$drillHosts = [];
|
||||||
|
if ($drillCountry) {
|
||||||
|
foreach ($countryStats as $c) {
|
||||||
|
if (strtoupper($c['code']) === strtoupper($drillCountry)) {
|
||||||
|
$drillHosts = $c['hosts'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$svgPath = __DIR__ . '/assets/img/world-map.svg';
|
||||||
|
$svgContent = '';
|
||||||
|
if (file_exists($svgPath)) {
|
||||||
|
$svgContent = file_get_contents($svgPath);
|
||||||
|
$styles = '';
|
||||||
|
$maxCountry = count($countryStats) > 0 ? $countryStats[0]['count'] : 1;
|
||||||
|
foreach ($countryStats as $c) {
|
||||||
|
if ($c['code'] === 'UNKNOWN') continue;
|
||||||
|
$ratio = $c['count'] / $maxCountry;
|
||||||
|
if ($ratio > 0.66) $fill = '#052c65';
|
||||||
|
elseif ($ratio > 0.33) $fill = '#0d6efd';
|
||||||
|
elseif ($ratio > 0.1) $fill = '#6ea8fe';
|
||||||
|
else $fill = '#cfe2ff';
|
||||||
|
$styles .= "#{$c['code']} { fill: {$fill}; }";
|
||||||
|
}
|
||||||
|
$svgContent = str_replace('</style>', $styles . '</style>', $svgContent);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-globe2 me-2"></i>Wereldkaart</h2>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="?page=worldmap&period=7" class="btn btn-outline-primary <?= $period === 7 ? 'active' : '' ?>">7 dagen</a>
|
||||||
|
<a href="?page=worldmap&period=30" class="btn btn-outline-primary <?= $period === 30 ? 'active' : '' ?>">30 dagen</a>
|
||||||
|
<a href="?page=worldmap&period=90" class="btn btn-outline-primary <?= $period === 90 ? 'active' : '' ?>">90 dagen</a>
|
||||||
|
<a href="?page=worldmap&period=365" class="btn btn-outline-primary <?= $period === 365 ? 'active' : '' ?>">365 dagen</a>
|
||||||
|
<a href="?page=worldmap&period=0" class="btn btn-outline-primary <?= $period === 0 ? 'active' : '' ?>">Alles</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($drillCountry): ?>
|
||||||
|
<div class="alert alert-info d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-funnel me-2"></i>Gefilterd op land: <strong><?= GeoIP::getCountryFlagEmoji($drillCountry) ?> <?= GeoIP::getCountryName($drillCountry) ?></strong></span>
|
||||||
|
<a href="?page=worldmap&period=<?= $period ?>" class="btn btn-sm btn-outline-danger"><i class="bi bi-x"></i> Filter wissen</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-primary-subtle text-primary"><i class="bi bi-globe"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= count($countryStats) ?></div>
|
||||||
|
<div class="stat-label">Landen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-success-subtle text-success"><i class="bi bi-envelope"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= number_format($totalLogs, 0, ',', '.') ?></div>
|
||||||
|
<div class="stat-label">Berichten</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-info-subtle text-info"><i class="bi bi-question-circle"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= $countryStats[0]['code'] === 'UNKNOWN' ? number_format($countryStats[0]['count'], 0, ',', '.') : '0' ?></div>
|
||||||
|
<div class="stat-label">Onbekend (geen GeoIP)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon bg-warning-subtle text-warning"><i class="bi bi-hdd-network"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value"><?= $sq->getUniqueHosts($period) ?></div>
|
||||||
|
<div class="stat-label">Unieke hosts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-map me-2"></i>Wereldkaart</span>
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="d-inline-block me-1" style="width:14px;height:14px;background:#cfe2ff;border-radius:2px;vertical-align:middle"></span> weinig
|
||||||
|
<span class="d-inline-block mx-1" style="width:14px;height:14px;background:#6ea8fe;border-radius:2px;vertical-align:middle"></span> gemiddeld
|
||||||
|
<span class="d-inline-block mx-1" style="width:14px;height:14px;background:#0d6efd;border-radius:2px;vertical-align:middle"></span> veel
|
||||||
|
<span class="d-inline-block mx-1" style="width:14px;height:14px;background:#052c65;border-radius:2px;vertical-align:middle"></span> meest
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3 position-relative world-map-wrapper">
|
||||||
|
<?php if ($svgContent): ?>
|
||||||
|
<div id="worldMapContainer" style="max-width:1000px;margin:0 auto;cursor:pointer;">
|
||||||
|
<?= $svgContent ?>
|
||||||
|
</div>
|
||||||
|
<div id="mapTooltip" class="map-tooltip" style="display:none;"></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>Geen wereldkaart SVG gevonden. Genereer deze met:
|
||||||
|
<code>php cli/generate-map.php</code>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($drillCountry && !empty($drillHosts)): ?>
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-hdd-network me-2"></i>Hosts in <?= GeoIP::getCountryName($drillCountry) ?></div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<?php foreach ($drillHosts as $host): ?>
|
||||||
|
<a href="?page=logs&host=<?= urlencode($host) ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<?= htmlspecialchars($host) ?>
|
||||||
|
<i class="bi bi-arrow-right text-muted"></i>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white"><i class="bi bi-list-columns me-2"></i>Landen (<?= count($countryStats) ?>)</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Land</th>
|
||||||
|
<th>Berichten</th>
|
||||||
|
<th>%</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php $maxC = $countryStats[0]['count'] ?? 1; ?>
|
||||||
|
<?php foreach ($countryStats as $i => $c): ?>
|
||||||
|
<tr class="<?= $drillCountry && strtoupper($c['code']) === strtoupper($drillCountry) ? 'table-active' : '' ?>">
|
||||||
|
<td class="text-muted"><?= $i + 1 ?></td>
|
||||||
|
<td>
|
||||||
|
<a href="?page=worldmap&period=<?= $period ?>&country=<?= urlencode($c['code']) ?>" class="text-decoration-none">
|
||||||
|
<?= GeoIP::getCountryFlagEmoji($c['code']) ?>
|
||||||
|
<?= GeoIP::getCountryName($c['code']) ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><?= number_format($c['count'], 0, ',', '.') ?></td>
|
||||||
|
<td><?= $totalLogs > 0 ? round($c['count'] / $totalLogs * 100, 1) : 0 ?>%</td>
|
||||||
|
<td>
|
||||||
|
<div class="progress" style="height:6px;min-width:80px">
|
||||||
|
<div class="progress-bar" style="width:<?= $c['count'] / $maxC * 100 ?>%"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var counts = <?= json_encode($mapCountries) ?>;
|
||||||
|
var container = document.getElementById('worldMapContainer');
|
||||||
|
var tooltip = document.getElementById('mapTooltip');
|
||||||
|
if (!container || !tooltip) return;
|
||||||
|
|
||||||
|
container.addEventListener('mousemove', function (e) {
|
||||||
|
var target = e.target.closest('.country');
|
||||||
|
if (!target) { tooltip.style.display = 'none'; return; }
|
||||||
|
var code = target.getAttribute('id');
|
||||||
|
var name = target.getAttribute('data-name') || code;
|
||||||
|
var n = counts[code] || 0;
|
||||||
|
tooltip.textContent = name + ': ' + n.toLocaleString('nl-NL') + ' bericht' + (n === 1 ? '' : 'en');
|
||||||
|
tooltip.style.display = 'block';
|
||||||
|
var r = container.getBoundingClientRect();
|
||||||
|
var x = e.clientX - r.left + 12;
|
||||||
|
var y = e.clientY - r.top + 12;
|
||||||
|
if (x + 200 > r.width) x = e.clientX - r.left - 200;
|
||||||
|
tooltip.style.left = x + 'px';
|
||||||
|
tooltip.style.top = y + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('mouseleave', function () {
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('click', function (e) {
|
||||||
|
var target = e.target.closest('.country');
|
||||||
|
if (!target) return;
|
||||||
|
var code = target.getAttribute('id');
|
||||||
|
if (code) {
|
||||||
|
window.location.href = '?page=worldmap&period=<?= $period ?>&country=' + encodeURIComponent(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php $sq = null; ?>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
namespace SyslogDash;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
|
class Database
|
||||||
|
{
|
||||||
|
private static ?PDO $instance = null;
|
||||||
|
|
||||||
|
public static function getInstance(): PDO
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
$config = self::getConfig();
|
||||||
|
try {
|
||||||
|
self::$instance = new PDO(
|
||||||
|
$config['dsn'],
|
||||||
|
$config['username'],
|
||||||
|
$config['password'],
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new \RuntimeException('Database verbinding mislukt: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getConfig(): array
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
return $cfg['db'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(): string
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
return $cfg['syslog']['table'] ?? 'SystemEvents';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getHostToIp(): array
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
return $cfg['syslog']['host_to_ip'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveHostnames(): bool
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
return $cfg['syslog']['resolve_hostnames'] ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
namespace SyslogDash;
|
||||||
|
|
||||||
|
class GeoIP
|
||||||
|
{
|
||||||
|
private static array $ipv4Cache = [];
|
||||||
|
private static array $ipv6Cache = [];
|
||||||
|
private static ?array $ipv4Ranges = null;
|
||||||
|
private static ?array $ipv6Ranges = null;
|
||||||
|
|
||||||
|
public static function lookupCountry(string $ip): ?string
|
||||||
|
{
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return self::lookupIPv4($ip);
|
||||||
|
}
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return self::lookupIPv6($ip);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function lookupIPv4(string $ip): ?string
|
||||||
|
{
|
||||||
|
if (isset(self::$ipv4Cache[$ip])) {
|
||||||
|
return self::$ipv4Cache[$ip];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$ipv4Ranges === null) {
|
||||||
|
self::$ipv4Ranges = self::loadRanges('ipv4.bin', 10, 'NNa2');
|
||||||
|
}
|
||||||
|
|
||||||
|
$long = ip2long($ip);
|
||||||
|
if ($long === false) return null;
|
||||||
|
|
||||||
|
foreach (self::$ipv4Ranges as $range) {
|
||||||
|
if ($long >= $range['start'] && $long <= $range['end']) {
|
||||||
|
self::$ipv4Cache[$ip] = $range['code'];
|
||||||
|
return $range['code'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$ipv4Cache[$ip] = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function lookupIPv6(string $ip): ?string
|
||||||
|
{
|
||||||
|
if (isset(self::$ipv6Cache[$ip])) {
|
||||||
|
return self::$ipv6Cache[$ip];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$ipv6Ranges === null) {
|
||||||
|
self::$ipv6Ranges = self::loadRanges('ipv6.bin', 34, 'a16a16a2');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bin = inet_pton($ip);
|
||||||
|
if ($bin === false) return null;
|
||||||
|
|
||||||
|
foreach (self::$ipv6Ranges as $range) {
|
||||||
|
if ($bin >= $range['start'] && $bin <= $range['end']) {
|
||||||
|
self::$ipv6Cache[$ip] = $range['code'];
|
||||||
|
return $range['code'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$ipv6Cache[$ip] = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function loadRanges(string $file, int $recordSize, string $format): array
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
$path = $cfg['geoip']['dbip_dir'] ?? __DIR__ . '/../storage/geoip';
|
||||||
|
$filePath = rtrim($path, '/') . '/' . $file;
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) return [];
|
||||||
|
|
||||||
|
$fh = fopen($filePath, 'rb');
|
||||||
|
if (!$fh) return [];
|
||||||
|
|
||||||
|
$ranges = [];
|
||||||
|
while (!feof($fh)) {
|
||||||
|
$record = fread($fh, $recordSize);
|
||||||
|
if ($record === false || strlen($record) < $recordSize) break;
|
||||||
|
|
||||||
|
if ($format === 'NNa2') {
|
||||||
|
$data = unpack('Nstart/Nend/a2code', $record);
|
||||||
|
$ranges[] = [
|
||||||
|
'start' => $data['start'],
|
||||||
|
'end' => $data['end'],
|
||||||
|
'code' => $data['code'],
|
||||||
|
];
|
||||||
|
} elseif ($format === 'a16a16a2') {
|
||||||
|
$data = unpack('a16start/a16end/a2code', $record);
|
||||||
|
$ranges[] = [
|
||||||
|
'start' => $data['start'],
|
||||||
|
'end' => $data['end'],
|
||||||
|
'code' => $data['code'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($fh);
|
||||||
|
return $ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCountryFlagEmoji(?string $code): string
|
||||||
|
{
|
||||||
|
if ($code === null || $code === '' || $code === 'UNKNOWN') {
|
||||||
|
return '🌍';
|
||||||
|
}
|
||||||
|
$code = strtoupper($code);
|
||||||
|
$regional = [0x1F1E6, 0x1F1E7, 0x1F1E8, 0x1F1E9, 0x1F1EA, 0x1F1EB, 0x1F1EC, 0x1F1ED, 0x1F1EE, 0x1F1EF, 0x1F1F0, 0x1F1F1, 0x1F1F2, 0x1F1F3, 0x1F1F4, 0x1F1F5, 0x1F1F6, 0x1F1F7, 0x1F1F8, 0x1F1F9, 0x1F1FA, 0x1F1FB, 0x1F1FC, 0x1F1FD, 0x1F1FE, 0x1F1FF];
|
||||||
|
$a = ord($code[0]) - 65;
|
||||||
|
$b = ord($code[1]) - 65;
|
||||||
|
if ($a < 0 || $a > 25 || $b < 0 || $b > 25) {
|
||||||
|
return '🌍';
|
||||||
|
}
|
||||||
|
return mb_chr($regional[$a]) . mb_chr($regional[$b]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCountryName($code, string $lang = 'nl'): string
|
||||||
|
{
|
||||||
|
$names = self::getCountryNames($lang);
|
||||||
|
if ($code === null || $code === '') return 'Onbekend';
|
||||||
|
$code = strtoupper($code);
|
||||||
|
return $names[$code] ?? $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCountryNames(string $lang = 'nl'): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'NL' => 'Nederland', 'DE' => 'Duitsland', 'BE' => 'België',
|
||||||
|
'FR' => 'Frankrijk', 'GB' => 'Verenigd Koninkrijk', 'US' => 'Verenigde Staten',
|
||||||
|
'CN' => 'China', 'RU' => 'Rusland', 'BR' => 'Brazilië',
|
||||||
|
'IN' => 'India', 'JP' => 'Japan', 'KR' => 'Zuid-Korea',
|
||||||
|
'IT' => 'Italië', 'ES' => 'Spanje', 'PT' => 'Portugal',
|
||||||
|
'PL' => 'Polen', 'SE' => 'Zweden', 'NO' => 'Noorwegen',
|
||||||
|
'DK' => 'Denemarken', 'FI' => 'Finland', 'AT' => 'Oostenrijk',
|
||||||
|
'CH' => 'Zwitserland', 'IE' => 'Ierland', 'LU' => 'Luxemburg',
|
||||||
|
'AU' => 'Australië', 'NZ' => 'Nieuw-Zeeland', 'CA' => 'Canada',
|
||||||
|
'SG' => 'Singapore', 'HK' => 'Hongkong', 'TW' => 'Taiwan',
|
||||||
|
'ZA' => 'Zuid-Afrika', 'AE' => 'Verenigde Arabische Emiraten',
|
||||||
|
'IL' => 'Israël', 'TR' => 'Turkije', 'GR' => 'Griekenland',
|
||||||
|
'CZ' => 'Tsjechië', 'SK' => 'Slowakije', 'HU' => 'Hongarije',
|
||||||
|
'RO' => 'Roemenië', 'BG' => 'Bulgarije', 'HR' => 'Kroatië',
|
||||||
|
'RS' => 'Servië', 'UA' => 'Oekraïne', 'BY' => 'Wit-Rusland',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hasDatabase(): bool
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
$dir = $cfg['geoip']['dbip_dir'] ?? __DIR__ . '/../storage/geoip';
|
||||||
|
return file_exists(rtrim($dir, '/') . '/ipv4.bin');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
namespace SyslogDash;
|
||||||
|
|
||||||
|
class Layout
|
||||||
|
{
|
||||||
|
public static function render(string $page, array $data = []): void
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../config.php';
|
||||||
|
$title = $cfg['dashboard']['title'] ?? 'Syslog Dash';
|
||||||
|
$themeColor = $cfg['dashboard']['theme_color'] ?? '#0a369d';
|
||||||
|
|
||||||
|
$navItems = [
|
||||||
|
'dashboard' => ['icon' => 'bi-speedometer2', 'label' => 'Dashboard'],
|
||||||
|
'worldmap' => ['icon' => 'bi-globe2', 'label' => 'Wereldkaart'],
|
||||||
|
'logs' => ['icon' => 'bi-list-ul', 'label' => 'Logs'],
|
||||||
|
'bi' => ['icon' => 'bi-bar-chart-line', 'label' => 'BI Rapporten'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$pageFile = __DIR__ . '/../pages/' . basename($page) . '.php';
|
||||||
|
$pageContent = '';
|
||||||
|
if (file_exists($pageFile)) {
|
||||||
|
extract($data);
|
||||||
|
ob_start();
|
||||||
|
require $pageFile;
|
||||||
|
$pageContent = ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPage = basename($page);
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title><?= $title ?> — <?= $navItems[$currentPage]['label'] ?? $currentPage ?></title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<link href="assets/css/style.css" rel="stylesheet">
|
||||||
|
<?php if ($currentPage === 'worldmap'): ?>
|
||||||
|
<style>
|
||||||
|
.country { fill: #dfe4ea; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: round; vector-effect: non-scaling-stroke; transition: fill .15s ease; cursor: pointer; }
|
||||||
|
.country:hover { fill: #ffc107 !important; }
|
||||||
|
</style>
|
||||||
|
<?php endif; ?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside class="admin-sidebar" style="background-color: <?= $themeColor ?>;">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<a href="?page=dashboard" class="text-white text-decoration-none">
|
||||||
|
<i class="bi bi-hdd-stack fs-4 me-2"></i><?= $title ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<div class="nav-section">Navigatie</div>
|
||||||
|
<?php foreach ($navItems as $key => $item): ?>
|
||||||
|
<a href="?page=<?= $key ?>" class="nav-link <?= $currentPage === $key ? 'active' : '' ?>">
|
||||||
|
<i class="bi <?= $item['icon'] ?> me-2"></i><?= $item['label'] ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<small>SyslogDash v1.0</small>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="admin-main">
|
||||||
|
<?= $pageContent ?>
|
||||||
|
</main>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||||
|
<script src="assets/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
<?php
|
||||||
|
namespace SyslogDash;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class SyslogQuery
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
private string $table;
|
||||||
|
private array $hostToIp;
|
||||||
|
private bool $resolveHostnames;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = Database::getInstance();
|
||||||
|
$this->table = Database::table();
|
||||||
|
$this->hostToIp = Database::getHostToIp();
|
||||||
|
$this->resolveHostnames = Database::resolveHostnames();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHostIps(): array
|
||||||
|
{
|
||||||
|
$ipMap = $this->hostToIp;
|
||||||
|
$hosts = $this->getDistinctHosts();
|
||||||
|
foreach ($hosts as $host) {
|
||||||
|
if (!isset($ipMap[$host])) {
|
||||||
|
if ($this->resolveHostnames && preg_match('/^\d+\.\d+\.\d+\.\d+$/', $host)) {
|
||||||
|
$ipMap[$host] = $host;
|
||||||
|
} elseif ($this->resolveHostnames) {
|
||||||
|
$resolved = gethostbyname($host);
|
||||||
|
if ($resolved !== $host && filter_var($resolved, FILTER_VALIDATE_IP)) {
|
||||||
|
$ipMap[$host] = $resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ipMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDistinctHosts(): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->query("SELECT DISTINCT `FromHost` FROM `{$this->table}` ORDER BY `FromHost`");
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCountryStats(int $days = 30): array
|
||||||
|
{
|
||||||
|
$geo = new GeoIP();
|
||||||
|
$hostIps = $this->getHostIps();
|
||||||
|
$countries = [];
|
||||||
|
|
||||||
|
$where = '';
|
||||||
|
$params = [];
|
||||||
|
if ($days > 0) {
|
||||||
|
$where = "WHERE `ReceivedAt` >= :since";
|
||||||
|
$params[':since'] = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT `FromHost`, COUNT(*) AS cnt
|
||||||
|
FROM `{$this->table}`
|
||||||
|
{$where}
|
||||||
|
GROUP BY `FromHost`
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$host = $row['FromHost'];
|
||||||
|
$ip = $hostIps[$host] ?? null;
|
||||||
|
$country = $ip ? $geo->lookupCountry($ip) : null;
|
||||||
|
$code = $country ?? 'UNKNOWN';
|
||||||
|
if (!isset($countries[$code])) {
|
||||||
|
$countries[$code] = ['code' => $code, 'count' => 0, 'hosts' => []];
|
||||||
|
}
|
||||||
|
$countries[$code]['count'] += (int)$row['cnt'];
|
||||||
|
$countries[$code]['hosts'][] = $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($countries, fn($a, $b) => $b['count'] <=> $a['count']);
|
||||||
|
return $countries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLogs(array $filters = [], int $page = 1, int $perPage = 50): array
|
||||||
|
{
|
||||||
|
$conditions = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($filters['country'])) {
|
||||||
|
$hostIps = $this->getHostIps();
|
||||||
|
$countryHosts = [];
|
||||||
|
$geo = new GeoIP();
|
||||||
|
foreach ($hostIps as $host => $ip) {
|
||||||
|
$c = $geo->lookupCountry($ip);
|
||||||
|
if (strtoupper($c) === strtoupper($filters['country'])) {
|
||||||
|
$countryHosts[] = $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($countryHosts)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($countryHosts), '?'));
|
||||||
|
$conditions[] = "`FromHost` IN ({$placeholders})";
|
||||||
|
$params = array_merge($params, $countryHosts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['host'])) {
|
||||||
|
$conditions[] = "`FromHost` = :host";
|
||||||
|
$params[':host'] = $filters['host'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['tag'])) {
|
||||||
|
$conditions[] = "`SysLogTag` LIKE :tag";
|
||||||
|
$params[':tag'] = '%' . $filters['tag'] . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['facility'])) {
|
||||||
|
$conditions[] = "`Facility` = :facility";
|
||||||
|
$params[':facility'] = (int)$filters['facility'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['priority'])) {
|
||||||
|
$conditions[] = "`Priority` = :priority";
|
||||||
|
$params[':priority'] = (int)$filters['priority'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$conditions[] = "`Message` LIKE :search";
|
||||||
|
$params[':search'] = '%' . $filters['search'] . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['from'])) {
|
||||||
|
$conditions[] = "`ReceivedAt` >= :from";
|
||||||
|
$params[':from'] = $filters['from'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['to'])) {
|
||||||
|
$conditions[] = "`ReceivedAt` <= :to";
|
||||||
|
$params[':to'] = $filters['to'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = !empty($conditions) ? 'WHERE ' . implode(' AND ', $conditions) : '';
|
||||||
|
|
||||||
|
$countStmt = $this->db->prepare("SELECT COUNT(*) FROM `{$this->table}` {$where}");
|
||||||
|
$countStmt->execute($params);
|
||||||
|
$total = (int)$countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
$order = ($filters['sort'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
$orderCol = 'ReceivedAt';
|
||||||
|
if (!empty($filters['sort_col'])) {
|
||||||
|
$allowed = ['ReceivedAt', 'DeviceReportedTime', 'FromHost', 'Facility', 'Priority', 'SysLogTag'];
|
||||||
|
if (in_array($filters['sort_col'], $allowed)) {
|
||||||
|
$orderCol = "`{$filters['sort_col']}`";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT `ID`, `ReceivedAt`, `DeviceReportedTime`, `FromHost`,
|
||||||
|
`Facility`, `Priority`, `Message`, `SysLogTag`
|
||||||
|
FROM `{$this->table}`
|
||||||
|
{$where}
|
||||||
|
ORDER BY {$orderCol} {$order}
|
||||||
|
LIMIT {$perPage} OFFSET {$offset}
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rows' => $rows,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'perPage' => $perPage,
|
||||||
|
'pages' => max(1, ceil($total / $perPage)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDailyStats(int $days = 30): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT DATE(`ReceivedAt`) AS d,
|
||||||
|
COUNT(*) AS cnt,
|
||||||
|
COUNT(DISTINCT `FromHost`) AS hosts
|
||||||
|
FROM `{$this->table}`
|
||||||
|
WHERE `ReceivedAt` >= :since
|
||||||
|
GROUP BY DATE(`ReceivedAt`)
|
||||||
|
ORDER BY d ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([':since' => date('Y-m-d H:i:s', strtotime("-{$days} days"))]);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$start = new \DateTime("-{$days} days");
|
||||||
|
$end = new \DateTime();
|
||||||
|
$period = new \DatePeriod($start, new \DateInterval('P1D'), $end);
|
||||||
|
$data = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$data[$r['d']] = ['cnt' => (int)$r['cnt'], 'hosts' => (int)$r['hosts']];
|
||||||
|
}
|
||||||
|
foreach ($period as $dt) {
|
||||||
|
$key = $dt->format('Y-m-d');
|
||||||
|
$result[] = [
|
||||||
|
'date' => $key,
|
||||||
|
'cnt' => $data[$key]['cnt'] ?? 0,
|
||||||
|
'hosts' => $data[$key]['hosts'] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHostStats(int $days = 30): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT `FromHost`, COUNT(*) AS cnt
|
||||||
|
FROM `{$this->table}`
|
||||||
|
WHERE `ReceivedAt` >= :since
|
||||||
|
GROUP BY `FromHost`
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
LIMIT 20
|
||||||
|
");
|
||||||
|
$stmt->execute([':since' => date('Y-m-d H:i:s', strtotime("-{$days} days"))]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTagStats(int $days = 30): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT `SysLogTag`, COUNT(*) AS cnt
|
||||||
|
FROM `{$this->table}`
|
||||||
|
WHERE `ReceivedAt` >= :since AND `SysLogTag` IS NOT NULL
|
||||||
|
GROUP BY `SysLogTag`
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
LIMIT 15
|
||||||
|
");
|
||||||
|
$stmt->execute([':since' => date('Y-m-d H:i:s', strtotime("-{$days} days"))]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeverityStats(int $days = 30): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT `Priority`, COUNT(*) AS cnt
|
||||||
|
FROM `{$this->table}`
|
||||||
|
WHERE `ReceivedAt` >= :since
|
||||||
|
GROUP BY `Priority`
|
||||||
|
ORDER BY `Priority` ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([':since' => date('Y-m-d H:i:s', strtotime("-{$days} days"))]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalCount(int $days = 0): int
|
||||||
|
{
|
||||||
|
$where = '';
|
||||||
|
$params = [];
|
||||||
|
if ($days > 0) {
|
||||||
|
$where = "WHERE `ReceivedAt` >= :since";
|
||||||
|
$params[':since'] = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||||
|
}
|
||||||
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM `{$this->table}` {$where}");
|
||||||
|
$stmt->execute($params);
|
||||||
|
return (int)$stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUniqueHosts(int $days = 0): int
|
||||||
|
{
|
||||||
|
$where = '';
|
||||||
|
$params = [];
|
||||||
|
if ($days > 0) {
|
||||||
|
$where = "WHERE `ReceivedAt` >= :since";
|
||||||
|
$params[':since'] = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||||
|
}
|
||||||
|
$stmt = $this->db->prepare("SELECT COUNT(DISTINCT `FromHost`) FROM `{$this->table}` {$where}");
|
||||||
|
$stmt->execute($params);
|
||||||
|
return (int)$stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKPIStats(int $days = 30): array
|
||||||
|
{
|
||||||
|
$total = $this->getTotalCount($days);
|
||||||
|
$uniqueHosts = $this->getUniqueHosts($days);
|
||||||
|
$countryStats = $this->getCountryStats($days);
|
||||||
|
$topCountry = $countryStats[0] ?? null;
|
||||||
|
$totalAll = $this->getTotalCount(0);
|
||||||
|
$avgPerDay = $days > 0 ? round($total / $days, 0) : 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $total,
|
||||||
|
'uniqueHosts' => $uniqueHosts,
|
||||||
|
'topCountry' => $topCountry,
|
||||||
|
'totalAll' => $totalAll,
|
||||||
|
'avgPerDay' => $avgPerDay,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runQuery(string $sql, array $params = []): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
if (preg_match('/^\s*SELECT/i', $sql)) {
|
||||||
|
return ['rows' => $stmt->fetchAll(), 'error' => null];
|
||||||
|
}
|
||||||
|
return ['rows' => ['affected' => $stmt->rowCount()], 'error' => null];
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
return ['rows' => [], 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDistinct(string $column): array
|
||||||
|
{
|
||||||
|
$allowed = ['FromHost', 'Facility', 'Priority', 'SysLogTag', 'EventLogType'];
|
||||||
|
if (!in_array($column, $allowed)) return [];
|
||||||
|
$stmt = $this->db->query("SELECT DISTINCT `{$column}` FROM `{$this->table}` ORDER BY `{$column}`");
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function facilityName(int $code): string
|
||||||
|
{
|
||||||
|
$names = [
|
||||||
|
0 => 'kern', 1 => 'user', 2 => 'mail', 3 => 'daemon',
|
||||||
|
4 => 'auth', 5 => 'syslog', 6 => 'lpr', 7 => 'news',
|
||||||
|
8 => 'uucp', 9 => 'cron', 10 => 'authpriv', 11 => 'ftp',
|
||||||
|
16 => 'local0', 17 => 'local1', 18 => 'local2', 19 => 'local3',
|
||||||
|
20 => 'local4', 21 => 'local5', 22 => 'local6', 23 => 'local7',
|
||||||
|
];
|
||||||
|
return $names[$code] ?? "facility_{$code}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function severityName(int $code): string
|
||||||
|
{
|
||||||
|
$names = [
|
||||||
|
0 => 'emerg', 1 => 'alert', 2 => 'crit', 3 => 'err',
|
||||||
|
4 => 'warning', 5 => 'notice', 6 => 'info', 7 => 'debug',
|
||||||
|
];
|
||||||
|
return $names[$code] ?? "severity_{$code}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function severityClass(int $code): string
|
||||||
|
{
|
||||||
|
return match ($code) {
|
||||||
|
0, 1, 2 => 'danger',
|
||||||
|
3, 4 => 'warning',
|
||||||
|
5 => 'info',
|
||||||
|
6, 7 => 'secondary',
|
||||||
|
default => 'secondary',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload.php @generated by Composer
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, $err);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException($err);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|
||||||
|
return ComposerAutoloaderInit0a0f92517e549816293da40320cffa7e::getLoader();
|
||||||
Vendored
+579
@@ -0,0 +1,579 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||||
|
*
|
||||||
|
* $loader = new \Composer\Autoload\ClassLoader();
|
||||||
|
*
|
||||||
|
* // register classes with namespaces
|
||||||
|
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||||
|
* $loader->add('Symfony', __DIR__.'/framework');
|
||||||
|
*
|
||||||
|
* // activate the autoloader
|
||||||
|
* $loader->register();
|
||||||
|
*
|
||||||
|
* // to enable searching the include path (eg. for PEAR packages)
|
||||||
|
* $loader->setUseIncludePath(true);
|
||||||
|
*
|
||||||
|
* In this example, if you try to use a class in the Symfony\Component
|
||||||
|
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||||
|
* the autoloader will first look for the class under the component/
|
||||||
|
* directory, and it will then fallback to the framework/ directory if not
|
||||||
|
* found before giving up.
|
||||||
|
*
|
||||||
|
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
* @see https://www.php-fig.org/psr/psr-0/
|
||||||
|
* @see https://www.php-fig.org/psr/psr-4/
|
||||||
|
*/
|
||||||
|
class ClassLoader
|
||||||
|
{
|
||||||
|
/** @var \Closure(string):void */
|
||||||
|
private static $includeFile;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $vendorDir;
|
||||||
|
|
||||||
|
// PSR-4
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, int>>
|
||||||
|
*/
|
||||||
|
private $prefixLengthsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private $prefixDirsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr4 = array();
|
||||||
|
|
||||||
|
// PSR-0
|
||||||
|
/**
|
||||||
|
* List of PSR-0 prefixes
|
||||||
|
*
|
||||||
|
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||||
|
*
|
||||||
|
* @var array<string, array<string, list<string>>>
|
||||||
|
*/
|
||||||
|
private $prefixesPsr0 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr0 = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $useIncludePath = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private $classMap = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $classMapAuthoritative = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>
|
||||||
|
*/
|
||||||
|
private $missingClasses = array();
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $apcuPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, self>
|
||||||
|
*/
|
||||||
|
private static $registeredLoaders = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $vendorDir
|
||||||
|
*/
|
||||||
|
public function __construct($vendorDir = null)
|
||||||
|
{
|
||||||
|
$this->vendorDir = $vendorDir;
|
||||||
|
self::initializeIncludeClosure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixes()
|
||||||
|
{
|
||||||
|
if (!empty($this->prefixesPsr0)) {
|
||||||
|
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixesPsr4()
|
||||||
|
{
|
||||||
|
return $this->prefixDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirs()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirsPsr4()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> Array of classname => path
|
||||||
|
*/
|
||||||
|
public function getClassMap()
|
||||||
|
{
|
||||||
|
return $this->classMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $classMap Class to filename map
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addClassMap(array $classMap)
|
||||||
|
{
|
||||||
|
if ($this->classMap) {
|
||||||
|
$this->classMap = array_merge($this->classMap, $classMap);
|
||||||
|
} else {
|
||||||
|
$this->classMap = $classMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix, either
|
||||||
|
* appending or prepending to the ones previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param list<string>|string $paths The PSR-0 root directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$this->fallbackDirsPsr0,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = $prefix[0];
|
||||||
|
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($prepend) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixesPsr0[$first][$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$this->prefixesPsr0[$first][$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace, either
|
||||||
|
* appending or prepending to the ones previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param list<string>|string $paths The PSR-4 base directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addPsr4($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
// Register directories for the root namespace.
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr4
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$this->fallbackDirsPsr4,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||||
|
// Register directories for a new namespace.
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||||
|
} elseif ($prepend) {
|
||||||
|
// Prepend directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixDirsPsr4[$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Append directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$this->prefixDirsPsr4[$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix,
|
||||||
|
* replacing any others previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param list<string>|string $paths The PSR-0 base directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr0 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace,
|
||||||
|
* replacing any others previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param list<string>|string $paths The PSR-4 base directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPsr4($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr4 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns on searching the include path for class files.
|
||||||
|
*
|
||||||
|
* @param bool $useIncludePath
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setUseIncludePath($useIncludePath)
|
||||||
|
{
|
||||||
|
$this->useIncludePath = $useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used to check if the autoloader uses the include path to check
|
||||||
|
* for classes.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getUseIncludePath()
|
||||||
|
{
|
||||||
|
return $this->useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off searching the prefix and fallback directories for classes
|
||||||
|
* that have not been registered with the class map.
|
||||||
|
*
|
||||||
|
* @param bool $classMapAuthoritative
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||||
|
{
|
||||||
|
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should class lookup fail if not found in the current class map?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isClassMapAuthoritative()
|
||||||
|
{
|
||||||
|
return $this->classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||||
|
*
|
||||||
|
* @param string|null $apcuPrefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setApcuPrefix($apcuPrefix)
|
||||||
|
{
|
||||||
|
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getApcuPrefix()
|
||||||
|
{
|
||||||
|
return $this->apcuPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @param bool $prepend Whether to prepend the autoloader or not
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register($prepend = false)
|
||||||
|
{
|
||||||
|
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||||
|
|
||||||
|
if (null === $this->vendorDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prepend) {
|
||||||
|
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||||
|
} else {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unregister()
|
||||||
|
{
|
||||||
|
spl_autoload_unregister(array($this, 'loadClass'));
|
||||||
|
|
||||||
|
if (null !== $this->vendorDir) {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given class or interface.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
* @return true|null True if loaded, null otherwise
|
||||||
|
*/
|
||||||
|
public function loadClass($class)
|
||||||
|
{
|
||||||
|
if ($file = $this->findFile($class)) {
|
||||||
|
$includeFile = self::$includeFile;
|
||||||
|
$includeFile($file);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to the file where the class is defined.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
*
|
||||||
|
* @return string|false The path if found, false otherwise
|
||||||
|
*/
|
||||||
|
public function findFile($class)
|
||||||
|
{
|
||||||
|
// class map lookup
|
||||||
|
if (isset($this->classMap[$class])) {
|
||||||
|
return $this->classMap[$class];
|
||||||
|
}
|
||||||
|
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||||
|
if ($hit) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->findFileWithExtension($class, '.php');
|
||||||
|
|
||||||
|
// Search for Hack files if we are running on HHVM
|
||||||
|
if (false === $file && defined('HHVM_VERSION')) {
|
||||||
|
$file = $this->findFileWithExtension($class, '.hh');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
apcu_add($this->apcuPrefix.$class, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $file) {
|
||||||
|
// Remember that this class does not exist.
|
||||||
|
$this->missingClasses[$class] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||||
|
*
|
||||||
|
* @return array<string, self>
|
||||||
|
*/
|
||||||
|
public static function getRegisteredLoaders()
|
||||||
|
{
|
||||||
|
return self::$registeredLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @param string $ext
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
private function findFileWithExtension($class, $ext)
|
||||||
|
{
|
||||||
|
// PSR-4 lookup
|
||||||
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
|
||||||
|
$first = $class[0];
|
||||||
|
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||||
|
$subPath = $class;
|
||||||
|
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||||
|
$subPath = substr($subPath, 0, $lastPos);
|
||||||
|
$search = $subPath . '\\';
|
||||||
|
if (isset($this->prefixDirsPsr4[$search])) {
|
||||||
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||||
|
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||||
|
if (file_exists($file = $dir . $pathEnd)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-4 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 lookup
|
||||||
|
if (false !== $pos = strrpos($class, '\\')) {
|
||||||
|
// namespaced class name
|
||||||
|
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||||
|
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||||
|
} else {
|
||||||
|
// PEAR-like class name
|
||||||
|
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->prefixesPsr0[$first])) {
|
||||||
|
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||||
|
if (0 === strpos($class, $prefix)) {
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 include paths.
|
||||||
|
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function initializeIncludeClosure()
|
||||||
|
{
|
||||||
|
if (self::$includeFile !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope isolated include.
|
||||||
|
*
|
||||||
|
* Prevents access to $this/self from included files.
|
||||||
|
*
|
||||||
|
* @param string $file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
self::$includeFile = \Closure::bind(static function($file) {
|
||||||
|
include $file;
|
||||||
|
}, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+396
@@ -0,0 +1,396 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
|
use Composer\Semver\VersionParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is copied in every Composer installed project and available to all
|
||||||
|
*
|
||||||
|
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||||
|
*
|
||||||
|
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class InstalledVersions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private static $selfDir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed[]|null
|
||||||
|
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||||
|
*/
|
||||||
|
private static $installed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $installedIsLocalDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
private static $canGetVendors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static $installedByVendor = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackages()
|
||||||
|
{
|
||||||
|
$packages = array();
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
$packages[] = array_keys($installed['versions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($packages)) {
|
||||||
|
return $packages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names with a specific type e.g. 'library'
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackagesByType($type)
|
||||||
|
{
|
||||||
|
$packagesByType = array();
|
||||||
|
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
foreach ($installed['versions'] as $name => $package) {
|
||||||
|
if (isset($package['type']) && $package['type'] === $type) {
|
||||||
|
$packagesByType[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packagesByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package is installed
|
||||||
|
*
|
||||||
|
* This also returns true if the package name is provided or replaced by another package
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @param bool $includeDevRequirements
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (isset($installed['versions'][$packageName])) {
|
||||||
|
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package satisfies a version constraint
|
||||||
|
*
|
||||||
|
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||||
|
*
|
||||||
|
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||||
|
*
|
||||||
|
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||||
|
* @param string $packageName
|
||||||
|
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||||
|
{
|
||||||
|
$constraint = $parser->parseConstraints((string) $constraint);
|
||||||
|
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||||
|
|
||||||
|
return $provided->matches($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||||
|
*
|
||||||
|
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||||
|
* whether a given version of a package is installed, and not just whether it exists
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string Version constraint usable with composer/semver
|
||||||
|
*/
|
||||||
|
public static function getVersionRanges($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges = array();
|
||||||
|
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' || ', $ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getPrettyVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||||
|
*/
|
||||||
|
public static function getReference($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['reference'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||||
|
*/
|
||||||
|
public static function getInstallPath($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||||
|
*/
|
||||||
|
public static function getRootPackage()
|
||||||
|
{
|
||||||
|
$installed = self::getInstalled();
|
||||||
|
|
||||||
|
return $installed[0]['root'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw installed.php data for custom implementations
|
||||||
|
*
|
||||||
|
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||||
|
*/
|
||||||
|
public static function getRawData()
|
||||||
|
{
|
||||||
|
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||||
|
self::$installed = include __DIR__ . '/installed.php';
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||||
|
*
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
public static function getAllRawData()
|
||||||
|
{
|
||||||
|
return self::getInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets you reload the static array from another file
|
||||||
|
*
|
||||||
|
* This is only useful for complex integrations in which a project needs to use
|
||||||
|
* this class but then also needs to execute another project's autoloader in process,
|
||||||
|
* and wants to ensure both projects have access to their version of installed.php.
|
||||||
|
*
|
||||||
|
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||||
|
* the data it needs from this class, then call reload() with
|
||||||
|
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||||
|
* the project in which it runs can then also use this class safely, without
|
||||||
|
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||||
|
*
|
||||||
|
* @param array[] $data A vendor/composer/installed.php data set
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||||
|
*/
|
||||||
|
public static function reload($data)
|
||||||
|
{
|
||||||
|
self::$installed = $data;
|
||||||
|
self::$installedByVendor = array();
|
||||||
|
|
||||||
|
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||||
|
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||||
|
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||||
|
// all installed packages for example
|
||||||
|
self::$installedIsLocalDir = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getSelfDir()
|
||||||
|
{
|
||||||
|
if (self::$selfDir === null) {
|
||||||
|
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$selfDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static function getInstalled()
|
||||||
|
{
|
||||||
|
if (null === self::$canGetVendors) {
|
||||||
|
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed = array();
|
||||||
|
$copiedLocalDir = false;
|
||||||
|
|
||||||
|
if (self::$canGetVendors) {
|
||||||
|
$selfDir = self::getSelfDir();
|
||||||
|
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||||
|
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||||
|
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||||
|
$installed[] = self::$installedByVendor[$vendorDir];
|
||||||
|
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require $vendorDir.'/composer/installed.php';
|
||||||
|
self::$installedByVendor[$vendorDir] = $required;
|
||||||
|
$installed[] = $required;
|
||||||
|
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||||
|
self::$installed = $required;
|
||||||
|
self::$installedIsLocalDir = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||||
|
$copiedLocalDir = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require __DIR__ . '/installed.php';
|
||||||
|
self::$installed = $required;
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||||
|
$installed[] = self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_classmap.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||||
|
);
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_namespaces.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
);
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_psr4.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'SyslogDash\\' => array($baseDir . '/src'),
|
||||||
|
);
|
||||||
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_real.php @generated by Composer
|
||||||
|
|
||||||
|
class ComposerAutoloaderInit0a0f92517e549816293da40320cffa7e
|
||||||
|
{
|
||||||
|
private static $loader;
|
||||||
|
|
||||||
|
public static function loadClassLoader($class)
|
||||||
|
{
|
||||||
|
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||||
|
require __DIR__ . '/ClassLoader.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Composer\Autoload\ClassLoader
|
||||||
|
*/
|
||||||
|
public static function getLoader()
|
||||||
|
{
|
||||||
|
if (null !== self::$loader) {
|
||||||
|
return self::$loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
require __DIR__ . '/platform_check.php';
|
||||||
|
|
||||||
|
spl_autoload_register(array('ComposerAutoloaderInit0a0f92517e549816293da40320cffa7e', 'loadClassLoader'), true, true);
|
||||||
|
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||||
|
spl_autoload_unregister(array('ComposerAutoloaderInit0a0f92517e549816293da40320cffa7e', 'loadClassLoader'));
|
||||||
|
|
||||||
|
require __DIR__ . '/autoload_static.php';
|
||||||
|
call_user_func(\Composer\Autoload\ComposerStaticInit0a0f92517e549816293da40320cffa7e::getInitializer($loader));
|
||||||
|
|
||||||
|
$loader->register(true);
|
||||||
|
|
||||||
|
return $loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_static.php @generated by Composer
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
class ComposerStaticInit0a0f92517e549816293da40320cffa7e
|
||||||
|
{
|
||||||
|
public static $prefixLengthsPsr4 = array (
|
||||||
|
'S' =>
|
||||||
|
array (
|
||||||
|
'SyslogDash\\' => 11,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $prefixDirsPsr4 = array (
|
||||||
|
'SyslogDash\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/../..' . '/src',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $classMap = array (
|
||||||
|
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
public static function getInitializer(ClassLoader $loader)
|
||||||
|
{
|
||||||
|
return \Closure::bind(function () use ($loader) {
|
||||||
|
$loader->prefixLengthsPsr4 = ComposerStaticInit0a0f92517e549816293da40320cffa7e::$prefixLengthsPsr4;
|
||||||
|
$loader->prefixDirsPsr4 = ComposerStaticInit0a0f92517e549816293da40320cffa7e::$prefixDirsPsr4;
|
||||||
|
$loader->classMap = ComposerStaticInit0a0f92517e549816293da40320cffa7e::$classMap;
|
||||||
|
|
||||||
|
}, null, ClassLoader::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"packages": [],
|
||||||
|
"dev": true,
|
||||||
|
"dev-package-names": []
|
||||||
|
}
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<?php return array(
|
||||||
|
'root' => array(
|
||||||
|
'name' => 'noorlander/syslogdash',
|
||||||
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => null,
|
||||||
|
'type' => 'project',
|
||||||
|
'install_path' => __DIR__ . '/../../',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev' => true,
|
||||||
|
),
|
||||||
|
'versions' => array(
|
||||||
|
'noorlander/syslogdash' => array(
|
||||||
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => null,
|
||||||
|
'type' => 'project',
|
||||||
|
'install_path' => __DIR__ . '/../../',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// platform_check.php @generated by Composer
|
||||||
|
|
||||||
|
$issues = array();
|
||||||
|
|
||||||
|
if (!(PHP_VERSION_ID >= 80100)) {
|
||||||
|
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($issues) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user