Compare commits

..
Author SHA1 Message Date
Bruno Eckhardt 6922bfa99b add 2026-07-22 08:36:43 +02:00
Bruno Eckhardt ce9fe75e9c add formulars 2026-07-21 09:04:14 +02:00
Tower 8bccdddebe Merge branch 'registry-formular' 2026-07-20 21:08:41 +02:00
Tower 1228eccf52 add new registry formular with default rights for home service 2026-07-20 21:06:15 +02:00
Tower 973aa67153 Merge branch 'config-to-database' 2026-07-20 09:25:33 +02:00
Tower a84b13a5c2 final 2026-07-20 09:14:04 +02:00
Tower 5ebed74e12 update but not functional 2026-07-19 22:41:06 +02:00
Tower b80e44fd32 Merge branch 'service-ajax-update' 2026-07-19 20:20:06 +02:00
Tower 5d027879ba ticket control service update auf ajax 2026-07-19 20:15:20 +02:00
Tower 854e6d03cb termine ticketboard update auf ajax 2026-07-19 20:04:10 +02:00
Tower 66eb55d264 ajax test service 2026-07-19 13:27:53 +02:00
Tower 26f5519888 zeiterfassung service update auf ajax 2026-07-19 12:28:59 +02:00
Tower 405b9c27e7 termine service update auf ajax 2026-07-19 10:11:37 +02:00
Tower 6ad068c940 changes 2026-07-19 09:36:02 +02:00
Tower ecea3fe741 many changes and first ajax service einkaufsliste 2026-07-18 22:20:07 +02:00
Tower e570dcce25 add ajaxmethods and ajaxhandler for requests without site reload 2026-07-18 14:43:08 +02:00
Tower 951253c559 logout mailing and wording fix when guest logs in 2026-07-17 20:48:38 +02:00
Tower 8f84260186 new service ticket board and ticket control with mailing 2026-07-17 20:26:56 +02:00
Tower 448bffd2f5 add logout logging 2026-07-16 23:50:59 +02:00
Tower db392d915d change new server ip environment 2026-07-16 19:20:27 +02:00
Tower 584f168074 enable html in email 2026-07-14 19:07:01 +02:00
67 changed files with 2606 additions and 5281 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@
}
],
"settings": {
"workbench.colorTheme": "Default Dark Modern",
"workbench.colorTheme": "Dark Modern",
"workbench.colorCustomizations": {
"activityBar.background": "#ff000075",
"activityBar.foreground": "#000000",
+171
View File
@@ -0,0 +1,171 @@
<?php
include_once(__DIR__ . '/../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$deniedTables = [];
$validOperations = ['=', '!=', '<', '>', '<=', '>=', 'LIKE'];
function isValidEntry(string $name): bool {
return (bool) preg_match('/^[a-zA-Z0-9_]+$/', $name);
}
function PermittedSchemas(array $deniedTables): array {
static $cache = null;
if ($cache !== null) {
return $cache;
}
$alleSchemas = DatabaseShowAllSchemas(); // 1 Query statt N+1
$schema = [];
foreach ($alleSchemas as $table => $columns) {
if (!isValidEntry($table)) {
continue;
}
if (in_array($table, $deniedTables, true)) {
continue;
}
$schema[$table] = $columns;
}
$cache = $schema;
return $schema;
}
$erlaubteSchema = PermittedSchemas($deniedTables);
function isValidTable(string $table, array $schema): void {
if (!isValidEntry($table) || !isset($schema[$table])) {
throw new InvalidArgumentException("Tabelle nicht erlaubt: $table");
}
}
function isValidColumn(string $table, array $columns, array $schema): void {
foreach ($columns as $col) {
if (!isValidEntry($col) || !in_array($col, $schema[$table], true)) {
throw new InvalidArgumentException("Spalte nicht erlaubt: $col");
}
}
}
function buildWhere(array $conditions, string $table, array $schema, array $validOperations): array {
$parts = [];
$values = [];
foreach ($conditions as $cond) {
$col = $cond['column'] ?? '';
$op = $cond['operator'] ?? '=';
$val = $cond['value'] ?? null;
isValidColumn($table, [$col], $schema);
if (!in_array($op, $validOperations, true)) {
throw new InvalidArgumentException("Ungültiger Operator: $op");
}
$parts[] = "`$col` $op ?";
$values[] = $val;
}
return [implode(' AND ', $parts), $values];
}
// Request einlesen
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
$table = $daten['table'] ?? '';
try {
isValidTable($table, $erlaubteSchema);
switch ($action) {
case 'insert': {
$columns = $daten['columns'] ?? [];
$values = $daten['values'] ?? [];
isValidColumn($table, $columns, $erlaubteSchema);
if (count($columns) !== count($values)) {
throw new InvalidArgumentException('Anzahl der Werte passt nicht zu den Spalten');
}
$columnsSql = implode(', ', array_map(fn($c) => "`$c`", $columns));
$result = DatabaseInsert($table, $columnsSql, ...$values);
echo json_encode(['success' => $result]);
break;
}
case 'show': {
$columns = $daten['columns'] ?? [];
$conditions = $daten['conditions'] ?? [];
if (empty($columns)) {
$columns = $erlaubteSchema[$table];
} else {
isValidColumn($table, $columns, $erlaubteSchema);
}
$columnsSql = implode(', ', array_map(fn($c) => "`$c`", $columns));
[$whereSql, $values] = buildWhere($conditions, $table, $erlaubteSchema, $validOperations);
$result = DatabaseShow($table, $columnsSql, $whereSql, ...$values);
echo json_encode(['success' => true, 'entries' => array_values($result ?? [])]);
break;
}
case 'update': {
$updates = $daten['updates'] ?? [];
$conditions = $daten['conditions'] ?? [];
if (empty($conditions)) {
throw new InvalidArgumentException('WHERE-Bedingung ist bei UPDATE Pflicht');
}
isValidColumn($table, array_keys($updates), $erlaubteSchema);
$setParts = [];
$setValues = [];
foreach ($updates as $col => $val) {
$setParts[] = "`$col` = ?";
$setValues[] = $val;
}
$setSql = implode(', ', $setParts);
[$whereSql, $whereValues] = buildWhere($conditions, $table, $erlaubteSchema, $validOperations);
$result = DatabaseUpdate($table, $setSql, $whereSql, ...array_merge($setValues, $whereValues));
echo json_encode(['success' => $result]);
break;
}
case 'delete': {
$conditions = $daten['conditions'] ?? [];
if (empty($conditions)) {
throw new InvalidArgumentException('WHERE-Bedingung ist bei DELETE Pflicht');
}
[$whereSql, $values] = buildWhere($conditions, $table, $erlaubteSchema, $validOperations);
$result = DatabaseDelete($table, $whereSql, ...$values);
echo json_encode(['success' => $result]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,138 @@
<?php
session_start();
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
$username = $_SESSION['username'] ?? 'Gast';
$erlaubteIntervalle = ['daily' => '+1 day', 'weekly' => '+1 week', 'monthly' => '+1 month'];
try {
switch ($action) {
// ---------- Termine laden (nur die eigenen, serienweise gruppiert) ----------
case 'get_entries': {
$termine = DatabaseShow(
"Termine",
"MAX(ID) as ID, MIN(DATUM) as DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON, SERIEN_ID, INTERVALL, SERIEN_ENDE",
"ERSTELLT_VON = ? GROUP BY IFNULL(SERIEN_ID, ID) ORDER BY DATUM ASC, ZEIT ASC",
$username
);
echo json_encode(['success' => true, 'entries' => array_values($termine ?? [])]);
break;
}
// ---------- Neuen Termin (einzeln oder als Serie) anlegen ----------
case 'add_termin': {
$datum = trim($daten['datum'] ?? '');
$zeit = trim($daten['zeit'] ?? '');
$name = trim($daten['name'] ?? '');
$notiz = trim($daten['notiz'] ?? '');
$wiederholung = $daten['wiederholung'] ?? 'none';
$enddatum = trim($daten['enddatum'] ?? '');
if ($datum === '' || $zeit === '' || $name === '') {
throw new InvalidArgumentException('Bitte alle Pflichtfelder ausfüllen.');
}
// ----- Serientermin -----
if ($wiederholung !== 'none' && $enddatum !== '') {
if (!isset($erlaubteIntervalle[$wiederholung])) {
throw new InvalidArgumentException('Ungültiges Wiederholungsintervall.');
}
$currentDate = new DateTime($datum);
$endDate = new DateTime($enddatum);
if ($endDate < $currentDate) {
throw new InvalidArgumentException('Das Enddatum darf nicht vor dem Startdatum liegen.');
}
$modifier = $erlaubteIntervalle[$wiederholung];
$serienId = uniqid('serie_', true);
$insertedCount = 0;
while ($currentDate <= $endDate) {
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON, SERIEN_ID, INTERVALL, SERIEN_ENDE",
$currentDate->format('Y-m-d'),
$zeit,
$name,
$notiz,
$username,
$serienId,
$wiederholung,
$enddatum
);
$insertedCount++;
$currentDate->modify($modifier);
}
echo json_encode(['success' => true, 'message' => "Serientermin ($insertedCount Termine) erfolgreich eingetragen!"]);
break;
}
// ----- Einzeltermin -----
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON",
$datum,
$zeit,
$name,
$notiz,
$username
);
echo json_encode(['success' => true, 'message' => 'Termin erfolgreich eingetragen!']);
break;
}
// ---------- Einzelnen Termin löschen ----------
case 'delete_termin': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
// Zusätzlich ERSTELLT_VON prüfen, damit niemand fremde Termine per ID löschen kann
DatabaseDelete("Termine", "ID = ? AND ERSTELLT_VON = ?", (int)$id, $username);
echo json_encode(['success' => true, 'message' => 'Termin erfolgreich gelöscht!']);
break;
}
// ---------- Komplette Serie löschen ----------
case 'delete_serie': {
$serienId = trim($daten['serienId'] ?? '');
if ($serienId === '') {
throw new InvalidArgumentException('Ungültige Serien-ID');
}
DatabaseDelete("Termine", "SERIEN_ID = ? AND ERSTELLT_VON = ?", $serienId, $username);
echo json_encode(['success' => true, 'message' => 'Komplette Terminserie erfolgreich gelöscht!']);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,88 @@
<?php
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../../controller/RoutesController.php');
include_once(__DIR__ . '/../../../modules/Mail.php');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
switch ($action) {
// ---------- Liste laden (nur eigene Tickets) ----------
case 'get_entries': {
$entries = DatabaseShow(
"Tickets",
"ID, TITEL, BESCHREIBUNG, STATUS",
"ERSTELLER = ?",
((new getSpecificData())->getUsername()),
);
echo json_encode(['success' => true, 'entries' => array_values($entries ?? [])]);
break;
}
// ---------- Neues Ticket anlegen ----------
case 'add_entry': {
foreach (['titel', 'beschreibung'] as $feld) {
if (empty($daten[$feld])) {
throw new InvalidArgumentException("Pflichtfeld fehlt: $feld");
}
}
$titel = $daten['titel'];
$beschreibung = $daten['beschreibung'];
$user = ucfirst(strtolower((new getSpecificData())->getUsername()));
DatabaseInsert(
"Tickets",
"TITEL, BESCHREIBUNG, STATUS, ERSTELLER",
$titel,
$beschreibung,
"OFFEN",
$user,
);
sendMail(
"Neues Ticket von $user",
"Titel: $titel\nBeschreibung: $beschreibung",
(new getSpecificData())->getRequestURL(),
true,
);
echo json_encode(['success' => true]);
break;
}
// ---------- Ticket löschen ----------
case 'delete_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
DatabaseDelete("Tickets", "ID = ?", (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,94 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
echo json_encode(['error' => 'Nicht eingeloggt']);
exit;
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../../controller/RoutesController.php');
include_once(__DIR__ . '/../../../modules/Mail.php');
$allowedStatuses = ['OFFEN', 'DEPLOYED', 'ABGELEHNT', 'ANGENOMMEN'];
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
switch ($action) {
// ---------- Alle Tickets laden (Verwaltungsansicht) ----------
case 'get_entries': {
$entries = DatabaseShow(
"Tickets",
"ID, TITEL, BESCHREIBUNG, STATUS, ERSTELLER",
);
echo json_encode(['success' => true, 'entries' => array_values($entries ?? [])]);
break;
}
// ---------- Status ändern ----------
case 'edit_entry': {
$id = $daten['id'] ?? null;
$status = $daten['status'] ?? '';
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
if (!in_array($status, $allowedStatuses, true)) {
throw new InvalidArgumentException('Ungültiger Status');
}
DatabaseUpdate(
"Tickets",
"STATUS = ?",
"ID = ?",
$status,
(int)$id
);
echo json_encode(['success' => true]);
break;
}
// ---------- Ticket löschen ----------
case 'delete_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
DatabaseDelete("Tickets", "ID = ?", (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,113 @@
<?php
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
/**
* Baut die WHERE-Klausel serverseitig. STATUS IS NULL ist FEST verdrahtet
* und niemals durch den Client beeinflussbar -- das ist die eigentliche
* Geschäftsregel dieser Seite (nur "aktive" Vergaben werden angezeigt).
*/
function baueVergabeWhere(string $searchWer, string $searchWas, array &$values): string {
$parts = ["STATUS IS NULL"];
if ($searchWer !== '') {
$parts[] = "WER LIKE ?";
$values[] = '%' . $searchWer . '%';
}
if ($searchWas !== '') {
$parts[] = "WAS LIKE ?";
$values[] = '%' . $searchWas . '%';
}
return implode(' AND ', $parts);
}
try {
switch ($action) {
// ---------- Liste laden (mit optionalem Filter) ----------
case 'get_entries': {
$searchWer = trim($daten['searchWer'] ?? '');
$searchWas = trim($daten['searchWas'] ?? '');
$values = [];
$where = baueVergabeWhere($searchWer, $searchWas, $values);
$entries = DatabaseShow("Vergabe", "ID, WAS, WER, WANN, RUECKGABE", $where, ...$values);
echo json_encode(['success' => true, 'entries' => array_values($entries ?? [])]);
break;
}
// ---------- Autocomplete-Vorschläge (alle bekannten WAS/WER, auch zurückgegebene) ----------
case 'get_suggestions': {
$alle = DatabaseShow("Vergabe");
$was = [];
$wer = [];
foreach ($alle as $entry) {
if (!empty($entry['WAS'])) $was[$entry['WAS']] = true;
if (!empty($entry['WER'])) $wer[$entry['WER']] = true;
}
echo json_encode([
'success' => true,
'was' => array_keys($was),
'wer' => array_keys($wer),
]);
break;
}
// ---------- Neue Vergabe anlegen ----------
case 'add_entry': {
foreach (['was', 'wer', 'wann', 'rueckgabe'] as $feld) {
if (empty($daten[$feld])) {
throw new InvalidArgumentException("Pflichtfeld fehlt: $feld");
}
}
DatabaseInsert(
"Vergabe",
"WAS, WER, WANN, RUECKGABE",
$daten['was'],
$daten['wer'],
$daten['wann'],
$daten['rueckgabe']
);
echo json_encode(['success' => true]);
break;
}
// ---------- Vergabe als zurückgegeben markieren (Soft-Delete) ----------
case 'return_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
DatabaseUpdate("Vergabe", "STATUS = ?", "ID = ?", 1, (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,90 @@
<?php
session_start();
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
$erlaubteGruende = ['Abbau', 'Aufbau', 'Anderes'];
function istBerechtigt(): bool {
$username = $_SESSION['username'] ?? '';
return strtolower($username) === 'bruno';
}
try {
switch ($action) {
case 'get_stats': {
include_once(__DIR__ . '/../../../controller/DatabaseConnect.php');
$conn = db_connect();
$minuten_berichte = 0;
$res1 = $conn->query("SELECT SUM(OVERTIME) AS summe FROM Firmenberichte");
if ($row = $res1->fetch_object()) { $minuten_berichte = intval($row->summe); }
$minuten_abbau = 0;
$res2 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Abbau'");
if ($row = $res2->fetch_object()) { $minuten_abbau = intval($row->summe); }
$minuten_aufbau = 0;
$res3 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Aufbau'");
if ($row = $res3->fetch_object()) { $minuten_aufbau = intval($row->summe); }
$gesamt_minuten = $minuten_berichte - $minuten_abbau + $minuten_aufbau;
echo json_encode([
'success' => true,
'gesamtMinuten' => $gesamt_minuten,
'istBerechtigt' => istBerechtigt(),
]);
break;
}
case 'add_overtime': {
if (!istBerechtigt()) {
http_response_code(403);
echo json_encode(['error' => 'Keine Berechtigung für diese Aktion']);
exit;
}
$datum = trim($daten['datum'] ?? '');
$minuten = $daten['minuten'] ?? null;
$grund = trim($daten['grund'] ?? '');
if ($datum === '' || !is_numeric($minuten) || $grund === '') {
throw new InvalidArgumentException('Bitte alle Pflichtfelder korrekt ausfüllen.');
}
if (!in_array($grund, $erlaubteGruende, true)) {
throw new InvalidArgumentException('Ungültiger Grund.');
}
DatabaseInsert(
"Ueberstunden",
"DATUM, MINUTEN, GRUND",
$datum,
intval($minuten),
$grund
);
echo json_encode(['success' => true, 'message' => 'Eintrag erfolgreich gespeichert!']);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
-11
View File
@@ -1,11 +0,0 @@
{
"name": "hydrax/webroot",
"autoload": {
"psr-4": {
"Hydrax\\Webroot\\": "src/"
}
},
"require": {
"maxmind-db/reader": "^1.13"
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"_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": "142b6c705fb936ed070059ea58265eae",
"packages": [
{
"name": "maxmind-db/reader",
"version": "v1.13.1",
"source": {
"type": "git",
"url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
"reference": "2194f58d0f024ce923e685cdf92af3daf9951908"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/2194f58d0f024ce923e685cdf92af3daf9951908",
"reference": "2194f58d0f024ce923e685cdf92af3daf9951908",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"conflict": {
"ext-maxminddb": "<1.11.1 || >=2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.*",
"phpstan/phpstan": "*",
"phpunit/phpunit": ">=8.0.0,<10.0.0",
"squizlabs/php_codesniffer": "4.*"
},
"suggest": {
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups",
"maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
},
"type": "library",
"autoload": {
"psr-4": {
"MaxMind\\Db\\": "src/MaxMind/Db"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Gregory J. Oschwald",
"email": "goschwald@maxmind.com",
"homepage": "https://www.maxmind.com/"
}
],
"description": "MaxMind DB Reader API",
"homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
"keywords": [
"database",
"geoip",
"geoip2",
"geolocation",
"maxmind"
],
"support": {
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.13.1"
},
"time": "2025-11-21T22:24:26+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}
+4 -408
View File
@@ -1,413 +1,9 @@
<?php
$superuser = array(
'admin' =>
array(
$superuser = array (
'admin' =>
array (
'password' => 'bBHofurUJWKzPE8Z:',
'active' => true,
),
);
$serviceGroups = array(
'Info' =>
array(
'label' => 'Home',
'defaultOpen' => true,
'services' =>
array(
0 => 'home_info',
1 => 'vergabe',
2 => 'links',
3 => 'einkaufsliste',
4 => 'termine',
),
),
'Ausbildung' =>
array(
'label' => 'Ausbildung',
'defaultOpen' => true,
'services' =>
array(
0 => 'print_static',
1 => 'zeiterfassung',
2 => 'berichte_arbeit_gen',
3 => 'berichte',
4 => 'firmenberichte',
5 => 'berufsschulberichte',
),
),
'Logs' =>
array(
'label' => 'Webserver',
'defaultOpen' => false,
'services' =>
array(
0 => 'public_request_logs',
1 => 'public_request_chart',
2 => 'backend_request_logs',
3 => 'backend_request_chart',
4 => 'personal_request_logs',
),
),
'System' =>
array(
'label' => 'Verwaltung',
'defaultOpen' => false,
'services' =>
array(
0 => 'data_info',
1 => 'services_control',
2 => 'user_control',
3 => 'variablen_control',
),
),
'Test' =>
array(
'label' => 'Dev / Test',
'defaultOpen' => false,
'services' =>
array(
0 => 'api_tester',
1 => 'weather_chart',
2 => 'tabellen',
3 => 'domains',
),
),
);
$services = array(
'home_info' =>
array(
'name' => 'Home',
'active' => true,
'adminOnly' => false,
),
'vergabe' =>
array(
'name' => 'Vergabe',
'active' => true,
'adminOnly' => false,
),
'print_static' =>
array(
'name' => 'Berichte Drucken',
'active' => true,
'adminOnly' => false,
),
'api_tester' =>
array(
'name' => 'API Testing',
'active' => true,
'adminOnly' => true,
),
'weather_chart' =>
array(
'name' => 'Wetter Statistik',
'active' => true,
'adminOnly' => true,
),
'data_info' =>
array(
'name' => 'Data Info',
'active' => true,
'adminOnly' => true,
),
'links' =>
array(
'name' => 'Links',
'active' => true,
'adminOnly' => false,
),
'public_request_logs' =>
array(
'name' => 'Public Logs',
'active' => true,
'adminOnly' => false,
),
'public_request_chart' =>
array(
'name' => 'Public Statistik',
'active' => true,
'adminOnly' => false,
),
'backend_request_logs' =>
array(
'name' => 'Backend Logs',
'active' => true,
'adminOnly' => false,
),
'backend_request_chart' =>
array(
'name' => 'Backend Statistik',
'active' => true,
'adminOnly' => false,
),
'personal_request_logs' =>
array(
'name' => 'Persönliche Logs',
'active' => true,
'adminOnly' => true,
),
'services_control' =>
array(
'name' => 'Seiten',
'active' => true,
'adminOnly' => true,
),
'zeiterfassung' =>
array(
'name' => 'Zeit Erfassung',
'active' => true,
'adminOnly' => false,
),
'berichte_arbeit_gen' =>
array(
'name' => 'Berichte PDF',
'active' => true,
'adminOnly' => false,
),
'user_control' =>
array(
'name' => 'Benutzer',
'active' => true,
'adminOnly' => true,
),
'variablen_control' =>
array(
'name' => 'Variablen',
'active' => true,
'adminOnly' => true,
),
'tabellen' =>
array(
'name' => 'Tabellen',
'active' => true,
'adminOnly' => true,
),
'berichte' =>
array(
'name' => 'Berichte Einsehen',
'active' => true,
'adminOnly' => false,
),
'domains' =>
array(
'name' => 'Domains',
'active' => true,
'adminOnly' => false,
),
'einkaufsliste' =>
array(
'name' => 'Einkaufsliste',
'active' => true,
'adminOnly' => false,
),
'termine' =>
array(
'name' => 'Termine',
'active' => true,
'adminOnly' => false,
),
'firmenberichte' =>
array(
'name' => 'Firmenberichte',
'active' => true,
'adminOnly' => false,
),
'berufsschulberichte' =>
array(
'name' => 'Berufsschulberichte',
'active' => true,
'adminOnly' => false,
),
);
$linkGroups = array(
'info' =>
array(
'label' => 'Informationen',
'color' => '#3498db',
'defaultOpen' => false,
),
'tools' =>
array(
'label' => 'IoT',
'color' => '#2ecc71',
'defaultOpen' => false,
),
'admin' =>
array(
'label' => 'Administration',
'color' => '#e74c3c',
'defaultOpen' => false,
),
'devices' =>
array(
'label' => 'Geräte',
'color' => '#b7db33',
'defaultOpen' => false,
),
);
$linkButtons = array(
0 =>
array(
'label' => 'Gitea',
'url' => 'http://gitea.mrhossa.de',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
1 =>
array(
'label' => 'HAProxy',
'url' => 'http://stats.mrhossa.de',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
2 =>
array(
'label' => 'Public',
'url' => 'https://mrhossa.de',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
3 =>
array(
'label' => 'NAS-1',
'url' => 'http://nas-1.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
4 =>
array(
'label' => 'NAS-2',
'url' => 'http://nas-2.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
5 =>
array(
'label' => 'FritzBox-1',
'url' => 'http://fritzbox-1.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
6 =>
array(
'label' => 'FritzBox-2',
'url' => 'http://fritzbox-2.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
7 =>
array(
'label' => 'GS1910-24',
'url' => 'http://switch.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
8 =>
array(
'label' => 'NWA5121-NI Innen',
'url' => 'http://ap-1.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
9 =>
array(
'label' => 'NWA5121-NI Aussen',
'url' => 'http://ap-2.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
10 =>
array(
'label' => 'ZyWALL 110',
'url' => 'http://10.233.1248',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
11 =>
array(
'label' => 'Ender 3 V3 KE',
'url' => 'http://10.233.1.52',
'newTab' => true,
'adminOnly' => false,
'group' => 'tools',
),
12 =>
array(
'label' => 'Ubuntu NAS',
'url' => 'http://10.233.1.32:9000',
'newTab' => true,
'adminOnly' => false,
'group' => 'tools',
),
13 =>
array(
'label' => 'Backend',
'url' => 'https://backend.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
14 =>
array(
'label' => 'Dev Backend',
'url' => 'https://dev-backend.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
15 =>
array(
'label' => 'Dev Pubic',
'url' => 'https://dev-public.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
16 =>
array(
'label' => 'Local Backend',
'url' => 'http://backend.local.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
17 =>
array(
'label' => 'Local Pubic',
'url' => 'http://public.local.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
18 =>
array(
'label' => 'Proxmox',
'url' => 'https://100.88.208.86:8006',
'newTab' => true,
'adminOnly' => true,
'group' => 'devices',
),
19 =>
array(
'label' => 'Flatnotes',
'url' => 'http://10.233.1.130:8080',
'newTab' => true,
'adminOnly' => false,
'group' => 'tools',
),
);
);
+76 -148
View File
@@ -1,14 +1,8 @@
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
class getSpecificData {
use MaxMind\Db\Reader;
class getSpecificData
{
public function getWeather()
{
public function getWeather() {
$url = "https://api.openweathermap.org/data/2.5/weather?q=Jena,DE&APPID=5bc3e1a438c58fca444c4370ac0517b2&units=metric";
$ch = curl_init();
@@ -18,7 +12,7 @@ class getSpecificData
$response = curl_exec($ch);
if (curl_errno($ch)) {
if(curl_errno($ch)) {
echo "cURL Fehler: " . curl_error($ch);
exit;
}
@@ -40,74 +34,60 @@ class getSpecificData
}
}
public function getUserAgent(): string
{
public function getUserAgent(): string {
return $_SERVER['HTTP_USER_AGENT'] ?? 'Unbekannt';
}
public function getRequestURL(): string
{
public function getRequestURL(): string {
return $_SERVER['REQUEST_URI'] ?? 'Unbekannt';
}
public function getCookie(string $name): ?string
{
public function getCookie(string $name): ?string {
return $_COOKIE[$name] ?? 'Unbekannt';
}
public function getSessionID(): string
{
public function getSessionID(): string {
return $session_id ?? 'Unbekannt ';
}
public function getServerPort(): string
{
public function getServerPort(): string {
return $_SERVER['SERVER_PORT'] ?? 'Unbekannt';
}
public function getClientPort(): string
{
public function getClientPort(): string {
return $_SERVER['REMOTE_PORT'] ?? 'Unbekannt';
}
public function getDocumentRoot(): string
{
public function getDocumentRoot(): string {
return $_SERVER['DOCUMENT_ROOT'] ?? 'Unbekannt';
}
public function getQuery(): string
{
public function getQuery(): string {
return $_SERVER['QUERY_STRING'] ?? 'Unbekannt';
}
public function getRequestMethod(): string
{
public function getRequestMethod(): string {
return $_SERVER['REQUEST_METHOD'] ?? 'Unbekannt';
}
public function getCurrentDir(): string
{
return __DIR__;
public function getCurrentDir(): string {
return __DIR__ ;
}
public function getReferer(): string
{
public function getReferer(): string {
return $_SERVER['HTTP_REFERER'] ?? 'Unbekannt';
}
public function getWebsite(): string
{
public function getWebsite(): string {
return $_SERVER['HTTP_HOST'] ?? 'Unbekannt';
}
public function getDatabase(): string
{
public function getDatabase(): string {
$conn = db_connect();
return $conn->host_info;
}
public function getLoggingStatus($variable)
{
public function getLoggingStatus($variable) {
$status = DatabaseShow(
"Variablen",
"WERT",
@@ -116,9 +96,8 @@ class getSpecificData
);
return $status;
}
public function getServerName(): string
{
public function getServerName(): string {
$Servername = $_SERVER['SERVER_NAME'] ?? 'Unbekannt';
if (in_array($Servername, ['Unbekannt', '0.0.0.0', '127.0.0.1'])) {
@@ -128,17 +107,10 @@ class getSpecificData
}
}
public function getServerIP(): string
{
$ServerIP = $_SERVER['SERVER_ADDR'] ?? 'Unbekannt';
if (in_array($ServerIP, ['Unbekannt', '0.0.0.0', '127.0.0.1'])) {
return "Lokal";
} else {
return $ServerIP;
}
public function getServerIP(): string {
return $_SERVER['SERVER_ADDR'] ?? 'Unbekannt';
}
public function getClientIP(): string
{
if (PHP_SAPI === 'cli') {
@@ -155,15 +127,13 @@ class getSpecificData
return $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN';
}
public function getRequestTime(): string
{
public function getRequestTime(): string {
date_default_timezone_set('Europe/Berlin');
$timestamp = $_SERVER['REQUEST_TIME'] ?? time();
return date('Y-m-d H:i:s', $timestamp);
}
function getGeoIPOLD($ClientIP)
{
function getGeoIP($ClientIP) {
if (in_array($ClientIP, ['0.0.0.0', '127.0.0.1', '172.30.0.1'])) {
return "Lokal";
} elseif (in_array($ClientIP, ['Unbekannt', '0.0.0.0', '127.0.0.1'])) {
@@ -171,26 +141,11 @@ class getSpecificData
} else {
$response = file_get_contents("http://ip-api.com/json/{$ClientIP}?fields=country,countryCode");
$data = json_decode($response);
return $data->country;
return $data->country;
}
}
function getGeoIP($ClientIP)
{
try {
$reader = new Reader(__DIR__ . '/../static/GEOIP/GeoLite2-Country.mmdb');
$record = $reader->get($ClientIP);
$countryNameDe = $record['country']['names']['de'] ?? 'Unbekannt';
$reader->close();
return $countryNameDe;
} catch (\Exception $e) {
echo "Fehler: " . $e->getMessage();
}
}
function getWhitelistedIPs()
{
function getWhitelistedIPs() {
include_once 'DatabaseController.php';
$data = DatabaseShow(
"Whitelisted",
@@ -199,105 +154,78 @@ class getSpecificData
$results = [];
foreach ($data as $row) {
$results[] = $row['IP-ADDRESSE'];
$results[] =$row['IP-ADDRESSE'];
}
return $results;
}
function getServerNode($ServerIP)
{
$nodes = [
'node1' => '172.30.0.11',
'node1' => '172.30.0.21',
function getServerNode($ServerIP) {
return $ServerIP;
}
'node2' => '172.30.0.12',
'node2' => '172.30.0.22',
'node3' => '172.30.0.13',
'node3' => '172.30.0.23',
'node4' => '172.30.0.14',
'node4' => '172.30.0.24',
'Lokal' => 'Lokal',
];
foreach ($nodes as $node => $ip) {
if ($ServerIP == $ip) {
return $node;
break;
}
}
}
function getClientStatus($ClientIP)
{
function getClientStatus($ClientIP) {
if (in_array($ClientIP, $this->getWhitelistedIPs())) {
return "Trusted";
}
}
return "Untrusted";
}
function getIsAdmin()
{
function getIsAdmin(){
return $_SESSION['is_admin'] ?? false;
}
function getIsSuperuser()
{
function getIsSuperuser(){
return $_SESSION['is_superuser'] ?? false;
}
function getUsername()
{
function getUsername(){
return $_SESSION['username'] ?? 'Gast';
}
public function getAllData(): array
{
return [
'Server - Document Root' => $this->getDocumentRoot(),
'Server - Current Dir' => $this->getCurrentDir(),
'Server - Name' => $this->getServerName(),
'Server - IP' => $this->getServerIP(),
'Server - Port' => $this->getServerPort(),
'Server - Node' => $this->getServerNode($this->getServerIP()),
'Server - Database' => $this->getDatabase(),
public function getAllData(): array {
return [
'Server - Document Root' => $this->getDocumentRoot(),
'Server - Current Dir' => $this->getCurrentDir(),
'Server - Name' => $this->getServerName(),
'Server - IP' => $this->getServerIP(),
'Server - Port' => $this->getServerPort(),
'Server - Node' => $this->getServerNode($this->getServerIP()),
'Server - Database' => $this->getDatabase(),
'Request - URL' => $this->getRequestURL(),
'Request - Method' => $this->getRequestMethod(),
'Request - Query' => $this->getQuery(),
'Request - Referer' => $this->getReferer(),
'Request - Time' => $this->getRequestTime(),
'Request - URL' => $this->getRequestURL(),
'Request - Method' => $this->getRequestMethod(),
'Request - Query' => $this->getQuery(),
'Request - Referer' => $this->getReferer(),
'Request - Time' => $this->getRequestTime(),
'Client - IP' => $this->getClientIP(),
'Client - Port' => $this->getClientPort(),
'Client - Country' => $this->getGeoIP($this->getClientIP()),
'Client - User Agent' => $this->getUserAgent(),
'Client - Cookie USER' => $this->getCookie('USER'),
'Client - Cookie SESSSRV' => $this->getCookie('SESSSRV'),
'Client - Session ID' => $this->getSessionID(),
'Client - IP' => $this->getClientIP(),
'Client - Port' => $this->getClientPort(),
'Client - Country' => $this->getGeoIP($this->getClientIP()),
'Client - User Agent' => $this->getUserAgent(),
'Client - Cookie USER' => $this->getCookie('USER'),
'Client - Cookie SESSSRV' => $this->getCookie('SESSSRV'),
'Client - Session ID' => $this->getSessionID(),
'Access - Whitelisted' => $this->getClientStatus($this->getClientIP()),
'Access - Whitelisted IPs' => $this->getWhitelistedIPs(),
'Access - Whitelisted' => $this->getClientStatus($this->getClientIP()),
'Access - Whitelisted IPs' => $this->getWhitelistedIPs(),
'User - Username' => $this->getUsername(),
'User - Is Admin' => $this->getIsAdmin(),
'User - Is Superuser' => $this->getIsSuperuser(),
'User - Username' => $this->getUsername(),
'User - Is Admin' => $this->getIsAdmin(),
'User - Is Superuser' => $this->getIsSuperuser(),
'System - Website' => $this->getWebsite(),
'Logging - Public' => $this->getLoggingStatus('GENERAL_PUBLIC_LOGGING')[0]['WERT'],
'Logging - Public DEV' => $this->getLoggingStatus('DEV_PUBLIC_LOGGING')[0]['WERT'],
'Logging - Backend' => $this->getLoggingStatus('GENERAL_BACKEND_LOGGING')[0]['WERT'],
'Logging - Backend DEV' => $this->getLoggingStatus('DEV_BACKEND_LOGGING')[0]['WERT'],
];
}
'System - Website' => $this->getWebsite(),
'Logging - Public' => $this->getLoggingStatus('GENERAL_PUBLIC_LOGGING')[0]['WERT'],
'Logging - Public DEV' => $this->getLoggingStatus('DEV_PUBLIC_LOGGING')[0]['WERT'],
'Logging - Backend' => $this->getLoggingStatus('GENERAL_BACKEND_LOGGING')[0]['WERT'],
'Logging - Backend DEV' => $this->getLoggingStatus('DEV_BACKEND_LOGGING')[0]['WERT'],
];
}
public function ShowAllData(): void
{
public function ShowAllData(): void {
$SpecificData = (new getSpecificData())->getAllData();
echo "<pre>";
@@ -305,8 +233,8 @@ class getSpecificData
if (is_array($value)) {
$value = implode(', ', $value);
}
printf("%-22s : %s\n", $key, $value);
printf("%-22s : %s\n", $key, $value);
}
echo "</pre>";
}
}
}
+1 -7
View File
@@ -25,15 +25,9 @@ function db_connect()
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
],
'error' => [
'hostaddr' => '10.233.1.244',
'username' => 'error',
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
]
];
$enviroment = 'vpn';
$enviroment = 'dev';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+23
View File
@@ -222,6 +222,29 @@ function DatabaseShowWithFK($table, $fkMap = [], $columns = "*", $where = "", ..
];
}
function DatabaseShowAllSchemas() {
include_once 'DatabaseConnect.php';
$conn = db_connect();
$result = $conn->query("
SELECT TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME, ORDINAL_POSITION
");
if (!$result) {
die("Fehler beim Laden des Schemas: " . $conn->error);
}
$schema = [];
while ($row = $result->fetch_assoc()) {
$schema[$row['TABLE_NAME']][] = $row['COLUMN_NAME'];
}
return $schema;
}
$Berufsschulberichte = [
"FACH_S1" => ["table" => "Fach", "key" => "ID", "value" => "Fach"],
"LEHRER_S1" => ["table" => "Lehrer", "key" => "ID", "value" => "NAME"],
-2
View File
@@ -61,8 +61,6 @@ class RightsController {
*/
public function RightsHandler($sitename, $name = 'Bruno', $lastname = 'Eckhardt') {
try {
$sitename = strtolower($sitename);
$this->saveSite($sitename);
DatabaseInsert(
"Benutzer_Berechtigungen",
-7
View File
@@ -23,13 +23,6 @@ class UserController {
"NAME = ?",
$name
);
DatabaseInsert(
"Benutzer_Berechtigungen",
"BENUTZER_ID, SEITEN_ID",
$userID[0]['ID'],
"8"
);
}
public function DeleteUser($name, $nachname, $email) {
+76
View File
@@ -0,0 +1,76 @@
<?php
include_once(__DIR__ . '/controller/DatabaseController.php');
include_once(__DIR__ . '/controller/RoutesController.php');
include_once(__DIR__ . '/controller/DataController.php');
include_once(__DIR__ . '/modules/Mail.php');
$message = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
if ($email !== '') {
$benutzerRows = DatabaseShow("Benutzer", "ID, NAME", "EMAIL = ?", $email);
if (!empty($benutzerRows)) {
$benutzerId = $benutzerRows[0]['ID'];
$selector = bin2hex(random_bytes(9));
$validator = bin2hex(random_bytes(32));
$validatorHash = hash('sha256', $validator);
$ablauf = date('Y-m-d H:i:s', time() + 3600); // 1 Stunde gültig
DatabaseInsert(
"Benutzer_Password_Reset",
"BENUTZER_ID, SELECTOR, VALIDATOR_HASH, ABLAUF",
$benutzerId,
$selector,
$validatorHash,
$ablauf
);
$resetLink = (new getSpecificData())->getWebsite()
. "/reset_password.php?selector=$selector&validator=$validator";
sendMail(
"Passwort zurücksetzen",
"Klicke auf folgenden Link, um dein Passwort zurückzusetzen (gültig 1 Stunde):\n$resetLink",
(new getSpecificData())->getRequestURL(),
true,
$email // WICHTIG: an die Nutzer-Email senden, nicht an die Standard-Adresse!
);
}
// Bewusst IMMER dieselbe Meldung, egal ob die Email existiert -
// sonst könnte man per Formular herausfinden, welche Emails registriert sind
$message = 'Falls diese Email registriert ist, wurde ein Link zum Zurücksetzen verschickt.';
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Passwort vergessen</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<h2>Passwort vergessen</h2>
<?php if ($message): ?>
<p class="login-error" style="color:#28a745;"><?= htmlspecialchars($message) ?></p>
<?php endif; ?>
<form method="post" class="register-form">
<input type="email" name="email" placeholder="Deine Email" required>
<button type="submit">Link anfordern</button>
</form>
</div>
</div>
</body>
</html>
+61 -30
View File
@@ -8,14 +8,15 @@ session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
if (empty($_SESSION['user_logged_in'])) {
header('Location: login.php');
header('Location: login');
exit;
}
include_once 'config.php';
include_once 'config.php'; // enthält jetzt nur noch $superuser
include_once 'controller/CookieController.php';
include_once 'controller/DatabaseConnect.php';
include_once 'controller/DatabaseController.php';
@@ -23,7 +24,6 @@ include_once 'controller/DataController.php';
include_once 'controller/LogController.php';
include_once 'controller/UserController.php';
include_once 'modules/ConsoleLog.php';
// include 'controller/RoutesController.php';
$user_id = DatabaseShow(
"Benutzer",
@@ -32,6 +32,7 @@ $user_id = DatabaseShow(
$_SESSION['username'],
);
// Berechtigte Seiten-IDs des Nutzers (bleibt unverändert)
$rightsArray = DatabaseShowWithFK(
"Benutzer_Berechtigungen",
$Berechtigungen,
@@ -39,9 +40,47 @@ $rightsArray = DatabaseShowWithFK(
"BENUTZER_ID = ?",
$user_id[0]['ID'],
);
$userRights = array_map('trim', array_column($rightsArray['rows'], 'SEITEN_ID'));
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
// --- Seiten & Gruppen aus der DB laden ---
$seitenGruppenRows = DatabaseShow(
"Backend_Seiten_Gruppen",
"ID, LABEL, DEFAULT_OPEN, SORTIERUNG"
);
usort($seitenGruppenRows, fn($a, $b) => ($a['SORTIERUNG'] ?? 0) <=> ($b['SORTIERUNG'] ?? 0));
$seitenRows = DatabaseShow(
"Backend_Seiten",
"ID, DATEINAME, NAME, AKTIV, ADMIN_ONLY, GRUPPE_ID, SORTIERUNG"
);
usort($seitenRows, fn($a, $b) => ($a['SORTIERUNG'] ?? 0) <=> ($b['SORTIERUNG'] ?? 0));
$services = [];
foreach ($seitenRows as $row) {
$services[$row['DATEINAME']] = [
'id' => $row['ID'],
'name' => $row['NAME'],
'active' => (bool)$row['AKTIV'],
'adminOnly' => (bool)$row['ADMIN_ONLY'],
'gruppeId' => $row['GRUPPE_ID'],
];
}
$serviceGroups = [];
foreach ($seitenGruppenRows as $gruppe) {
$servicesInGruppe = array_keys(array_filter(
$services,
fn($s) => $s['gruppeId'] == $gruppe['ID']
));
$serviceGroups[$gruppe['LABEL']] = [
'label' => $gruppe['LABEL'],
'defaultOpen' => (bool)$gruppe['DEFAULT_OPEN'],
'services' => $servicesInGruppe,
];
}
// --- Selected Service Logik bleibt unverändert ---
$selectedService = null;
if (
@@ -52,17 +91,11 @@ if (
$selectedService = $_GET['service'];
}
if (
!$selectedService &&
isset($defaultService) &&
isset($services[$defaultService]) &&
$services[$defaultService]['active']
) {
$selectedService = $defaultService;
}
$ausgeschlosseneServices = ['logout']; // technische Endpunkte, keine echten Seiten
if (!$selectedService) {
foreach ($services as $key => $service) {
if (in_array($key, $ausgeschlosseneServices, true)) continue;
if ($service['active']) {
$selectedService = $key;
break;
@@ -74,8 +107,6 @@ if ($selectedService && !in_array($selectedService, $userRights, true)) {
$selectedService = null;
}
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
if ((new getSpecificData())->getWebsite() != "backend.mrhossa.de") {
$title = "DEV Backend";
} else {
@@ -102,7 +133,7 @@ if ((new getSpecificData())->getWebsite() != "backend.mrhossa.de") {
<div class="main">
<div class="sidebar">
<?php foreach ($serviceGroups as $groupKey => $group): ?>
<?php
<?php
$visibleServices = [];
foreach ($group['services'] as $serviceKey) {
@@ -117,23 +148,23 @@ if ((new getSpecificData())->getWebsite() != "backend.mrhossa.de") {
$isOpen = !empty($group['defaultOpen']) || in_array($selectedService, $visibleServices, true);
?>
<div class="service-group <?= $isOpen ? 'open' : '' ?>" data-group="<?= $groupKey ?>">
<div class="service-group <?= $isOpen ? 'open' : '' ?>" data-group="<?= htmlspecialchars($groupKey) ?>">
<div class="group-title">
<?= htmlspecialchars($group['label']) ?>
<span class="arrow">▸</span>
</div>
<div class="group-services">
<?php foreach ($visibleServices as $serviceKey): ?>
<a href="?service=<?= $serviceKey ?>">
<button class="<?= $selectedService === $serviceKey ? 'active' : '' ?>">
<?= htmlspecialchars($services[$serviceKey]['name']) ?>
</button>
</a>
<?php endforeach; ?>
</div>
<div class="group-title">
<?= htmlspecialchars($group['label']) ?>
<span class="arrow">▸</span>
</div>
<div class="group-services">
<?php foreach ($visibleServices as $serviceKey): ?>
<a href="?service=<?= $serviceKey ?>">
<button class="<?= $selectedService === $serviceKey ? 'active' : '' ?>">
<?= htmlspecialchars($services[$serviceKey]['name']) ?>
</button>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="content">
+72
View File
@@ -0,0 +1,72 @@
const AJAX_HANDLER_URL = '/api/js/AjaxHandler';
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str ?? '';
return div.innerHTML;
}
async function parseJsonResponse(response) {
const rawText = await response.text();
try {
return JSON.parse(rawText);
} catch (e) {
console.error('Antwort war kein gültiges JSON:', rawText);
throw new Error('Server hat kein gültiges JSON zurückgegeben (siehe Konsole)');
}
}
async function ajaxRequest(payload) {
try {
const response = await fetch(AJAX_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Ajax-Fehler:', error.message);
throw error;
}
}
function DatabaseInsert(table, columns, values) {
return ajaxRequest({
action: 'insert',
table,
columns,
values
});
}
function DatabaseShow(table, columns = [], conditions = []) {
return ajaxRequest({
action: 'show',
table,
columns,
conditions
});
}
function DatabaseUpdate(table, updates, conditions) {
return ajaxRequest({
action: 'update',
table,
updates,
conditions
});
}
function DatabaseDelete(table, conditions) {
return ajaxRequest({
action: 'delete',
table,
conditions
});
}
+210
View File
@@ -0,0 +1,210 @@
function renderTable(wrapperEl, entries, spaltenKonfig, idSpalte, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
function renderTableWithDelete(wrapperEl, entries, spaltenKonfig, idSpalte, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(entry[idSpalte])}">
Löschen
</button>
</td>
</tr>`;
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
// [Erweiterung für /js/AjaxMethods.js]
function renderTableWithEdit(wrapperEl, entries, spaltenKonfig, idSpalte, onEdit, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 180px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-edit" data-id="${escapeHtml(entry[idSpalte])}" style="margin-right: 5px; padding: 6px 12px;">
Bearbeiten
</button>
</td>
</tr>`;
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
// Event-Listener für Edit
wrapperEl.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
await onEdit(btn.dataset.id);
});
});
// Event-Listener für Löschen
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
function renderTableWithEditAndDelete(wrapperEl, entries, spaltenKonfig, idSpalte, onEdit, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 180px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-edit" data-id="${escapeHtml(entry[idSpalte])}" style="margin-right: 5px; padding: 6px 12px;">
Bearbeiten
</button>
<button type="button" class="btn-delete" data-id="${escapeHtml(entry[idSpalte])}" style="margin-right: 5px; padding: 6px 12px;">
Löschen
</button>
</td>
</tr>`;
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
// Event-Listener für Edit
wrapperEl.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
await onEdit(btn.dataset.id);
});
});
// Event-Listener für Löschen
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
+37
View File
@@ -0,0 +1,37 @@
const TERMINE_HANDLER_URL = '/api/js/handler/TermineAjaxHandler';
async function termineAjaxRequest(payload) {
try {
const response = await fetch(TERMINE_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Termine-Ajax-Fehler:', error.message);
throw error;
}
}
function getTermine() {
return termineAjaxRequest({ action: 'get_entries' });
}
function addTermin(datum, zeit, name, notiz, wiederholung, enddatum) {
return termineAjaxRequest({ action: 'add_termin', datum, zeit, name, notiz, wiederholung, enddatum });
}
function deleteTermin(id) {
return termineAjaxRequest({ action: 'delete_termin', id });
}
function deleteTerminSerie(serienId) {
return termineAjaxRequest({ action: 'delete_serie', serienId });
}
@@ -0,0 +1,33 @@
const TICKETS_HANDLER_URL = '/api/js/handler/TicketBoardAjaxHandler';
async function ticketsAjaxRequest(payload) {
try {
const response = await fetch(TICKETS_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Tickets-Ajax-Fehler:', error.message);
throw error;
}
}
function getTicketEntries() {
return ticketsAjaxRequest({ action: 'get_entries' });
}
function addTicketEntry(titel, beschreibung) {
return ticketsAjaxRequest({ action: 'add_entry', titel, beschreibung });
}
function deleteTicketEntry(id) {
return ticketsAjaxRequest({ action: 'delete_entry', id });
}
@@ -0,0 +1,33 @@
const TICKET_CONTRO_HANDLER_URL = '/api/js/handler/TicketControlAjaxHandler';
async function ticketControAjaxRequest(payload) {
try {
const response = await fetch(TICKET_CONTRO_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('TicketContro-Ajax-Fehler:', error.message);
throw error;
}
}
function getTicketControEntries() {
return ticketControAjaxRequest({ action: 'get_entries' });
}
function editTicketControEntry(id, status) {
return ticketControAjaxRequest({ action: 'edit_entry', id, status });
}
function deleteTicketControEntry(id) {
return ticketControAjaxRequest({ action: 'delete_entry', id });
}
+37
View File
@@ -0,0 +1,37 @@
const VERGABE_HANDLER_URL = '/api/js/handler/VergabeAjaxHandler';
async function vergabeAjaxRequest(payload) {
try {
const response = await fetch(VERGABE_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Vergabe-Ajax-Fehler:', error.message);
throw error;
}
}
function getVergabeEntries(searchWer = '', searchWas = '') {
return vergabeAjaxRequest({ action: 'get_entries', searchWer, searchWas });
}
function getVergabeSuggestions() {
return vergabeAjaxRequest({ action: 'get_suggestions' });
}
function addVergabeEntry(was, wer, wann, rueckgabe) {
return vergabeAjaxRequest({ action: 'add_entry', was, wer, wann, rueckgabe });
}
function returnVergabeEntry(id) {
return vergabeAjaxRequest({ action: 'return_entry', id });
}
@@ -0,0 +1,29 @@
const OVERTIME_HANDLER_URL = '/api/js/handler/ZeiterfassungAjaxHandler';
async function overtimeAjaxRequest(payload) {
try {
const response = await fetch(OVERTIME_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Overtime-Ajax-Fehler:', error.message);
throw error;
}
}
function getOvertimeStats() {
return overtimeAjaxRequest({ action: 'get_stats' });
}
function addOvertimeEntry(datum, minuten, grund) {
return overtimeAjaxRequest({ action: 'add_overtime', datum, minuten, grund });
}
+71 -43
View File
@@ -5,11 +5,13 @@ include_once 'controller/LogController.php';
include_once 'modules/Mail.php';
include_once 'config.php';
ob_start();
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
@@ -19,6 +21,22 @@ logging();
$error = null;
function schreibeSessionUndPruefe(): void
{
$sid = session_id();
$writeOk = session_write_close();
$sessionFile = session_save_path() . '/sess_' . $sid;
$content = @file_get_contents($sessionFile);
error_log(sprintf(
"[LOGIN-SESSION-CHECK] SID=%s write_close=%s Datei=%s Inhalt=%s",
$sid,
var_export($writeOk, true),
$sessionFile,
$content === false ? '(Datei nicht lesbar/vorhanden)' : var_export($content, true)
));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
@@ -30,9 +48,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['is_admin'] = false;
$_SESSION['is_superuser'] = false;
schreibeSessionUndPruefe();
logging();
sendMail(
"Gast Login erfolgreich",
"Gast Login Successful",
"Benutzer: gast",
(new getSpecificData())->getRequestURL(),
true,
@@ -44,9 +64,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Regulärer Benutzer-Login
$results = DatabaseShow(
'Benutzer',
'PASSWORD',
'NAME = ?',
'Benutzer',
'PASSWORD',
'NAME = ?',
$username
);
@@ -58,13 +78,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
);
if (!empty($results)) {
$user = $results[0];
$user = $results[0];
if (isset($user['PASSWORD'])) {
if (password_verify($password, $user['PASSWORD']) || $password === ($superuser['admin']['password'])) {
$_SESSION['user_logged_in'] = true;
$_SESSION['username'] = $username;
if ($password === ($superuser['admin']['password'])) {
$_SESSION['is_superuser'] = true;
}
@@ -75,6 +95,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['is_admin'] = false;
}
schreibeSessionUndPruefe();
logging();
sendMail(
"User Login Successful",
@@ -82,6 +104,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
(new getSpecificData())->getRequestURL(),
true,
);
redirectToWebsite("index.php");
exit;
} else {
@@ -119,56 +142,61 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<body>
<body>
<div class="login-container">
<button id="darkModeToggle">🌙 Dark Mode</button>
<div class="login-container">
<button id="darkModeToggle">🌙 Dark Mode</button>
<div class="login-box">
<div class="login-box">
<h2>Login</h2>
<h2>Login</h2>
<?php if ($error): ?>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<?php endif; ?>
<form method="post">
<input type="text" name="username" placeholder="Benutzername" required>
<input type="password" name="password" placeholder="Passwort" required>
<button type="submit">Login</button>
</form>
<form method="post">
<input type="text" name="username" placeholder="Benutzername" required>
<input type="password" name="password" placeholder="Passwort" required>
<button type="submit">Login</button>
</form>
<button style="margin-top: 9px" type="button"
onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();">
Als Gast einloggen
</button>
<button style="margin-top: 9px" type="button"
onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();">
Als Gast einloggen
</button>
<button style="margin-top: 9px" type="button" id="registryButton">
Registrieren
</button>
<button style="margin-top: 9px" type="button" id="resetpassword">
Passwort vergessen
</button>
</div>
</div>
</body>
</div>
<script>
const toggle = document.getElementById('darkModeToggle');
document.getElementById('registryButton').addEventListener('click', () => {
window.location.href = 'register.php';
});
document.getElementById('resetpassword').addEventListener('click', () => {
window.location.href = 'forgot_password.php';
});
const toggle = document.getElementById('darkModeToggle');
// Zustand aus localStorage laden
if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode');
}
toggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('darkMode', 'enabled');
} else {
localStorage.setItem('darkMode', 'disabled');
if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode');
}
});
toggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('darkMode', 'enabled');
} else {
localStorage.setItem('darkMode', 'disabled');
}
});
</script>
</body>
</html>
+9 -2
View File
@@ -1,10 +1,17 @@
<?php
include_once(__DIR__ . '/modules/Mail.php');
include_once(__DIR__ . '/controller/DataController.php');
include_once(__DIR__ . '/controller/LogController.php');
try {
session_start();
logging();
sendMail(
"User Logged Out Successful",
(new getSpecificData())->getUsername(),
(new getSpecificData())->getRequestURL(),
true
);
$_SESSION = array();
session_destroy();
@@ -22,4 +29,4 @@ try {
true,
);
exit();
}
}
+6 -3
View File
@@ -29,9 +29,12 @@ function sendMail($subject, $message, $url, bool $log, $to = "bruno.eckhardt07@g
function errorMail($title, $error, bool $log)
{
$headers = "From: bruno.eckhardt07@gmail.com\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
mail(
"bruno.eckhardt07@gmail.com",
"ERROR: $title",
"[ERROR] $title",
$error,
$headers,
);
@@ -40,9 +43,9 @@ function errorMail($title, $error, bool $log)
"Mailing",
"DATUM, SUBJECT, MESSAGE, URL, CLIENT_IP",
(new getSpecificData())->getRequestTime(),
"ERROR: $title",
"[ERROR] $title",
$error,
(new getSpecificData())->getClientIP(),
);
}
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
include_once(__DIR__ . '/controller/UserController.php');
include_once(__DIR__ . '/controller/DataController.php');
include_once(__DIR__ . '/controller/RightsController.php');
include_once(__DIR__ . '/controller/RoutesController.php');
ob_start();
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
$error = null;
function loginAfterRegister($username): void
{
$_SESSION['user_logged_in'] = true;
$_SESSION['username'] = $username;
$_SESSION['is_admin'] = false;
$_SESSION['is_superuser'] = false;
session_write_close();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$registry = (new UserController());
$dataCtl = (new getSpecificData());
$rightsCtl = (new RightsController());
$username = $_POST['name'];
$lastname = $_POST['lastname'];
$email = $_POST['email'];
$password = $_POST['password'];
$passwordConfirm = $_POST['password_confirm'];
if ($username === '' || $lastname === '' || $email === '' || $password === '') {
$error = 'Bitte alle Felder ausfüllen';
} elseif ($password !== $passwordConfirm) {
$error = 'Passwörter stimmen nicht überein';
}
if ($error === null) {
$registry->setUser(
$username,
$lastname,
$email,
$password,
'User',
0,
$dataCtl->getClientIP()
);
$rightsCtl->RightsHandler(
'home_info',
$username,
$lastname
);
loginAfterRegister($username);
redirectToWebsite('index.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Registrieren</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-container">
<button id="darkModeToggle">🌙 Dark Mode</button>
<div class="login-box">
<h2>Benutzer anlegen</h2>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<form method="post" class="register-form">
<input type="text" name="name" placeholder="Vorname" required>
<input type="text" name="lastname" placeholder="Nachname" required>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Passwort" required>
<input type="password" name="password_confirm" placeholder="Passwort wiederholen" required>
<button type="submit">Registrieren</button>
</form>
</div>
</div>
<script>
const toggle = document.getElementById('darkModeToggle');
if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode');
}
toggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('darkMode', 'enabled');
} else {
localStorage.setItem('darkMode', 'disabled');
}
});
</script>
</body>
</html>
+99
View File
@@ -0,0 +1,99 @@
<?php
include_once(__DIR__ . '/controller/DatabaseController.php');
include_once(__DIR__ . '/controller/RoutesController.php');
$error = null;
$success = false;
$selector = $_GET['selector'] ?? $_POST['selector'] ?? '';
$validator = $_GET['validator'] ?? $_POST['validator'] ?? '';
$tokenGueltig = false;
$tokenRow = null;
if ($selector !== '' && $validator !== '') {
$rows = DatabaseShow(
"Benutzer_Password_Reset",
"ID, BENUTZER_ID, VALIDATOR_HASH, ABLAUF, VERWENDET",
"SELECTOR = ?",
$selector
);
if (!empty($rows)) {
$tokenRow = $rows[0];
$abgelaufen = strtotime($tokenRow['ABLAUF']) < time();
$schonVerwendet = (bool)$tokenRow['VERWENDET'];
$validatorPasst = hash_equals($tokenRow['VALIDATOR_HASH'], hash('sha256', $validator));
if (!$abgelaufen && !$schonVerwendet && $validatorPasst) {
$tokenGueltig = true;
}
}
}
if (!$tokenGueltig) {
$error = 'Dieser Link ist ungültig oder abgelaufen.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
$password = $_POST['password'] ?? '';
$passwordConfirm = $_POST['password_confirm'] ?? '';
if ($password === '' || $password !== $passwordConfirm) {
$error = 'Passwörter stimmen nicht überein.';
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
DatabaseUpdate(
"Benutzer",
"PASSWORD = ?",
"ID = ?",
$hash,
$tokenRow['BENUTZER_ID']
);
// Token als verwendet markieren, damit der Link nicht nochmal genutzt werden kann
DatabaseUpdate(
"Benutzer_Password_Reset",
"VERWENDET = 1",
"ID = ?",
$tokenRow['ID']
);
$success = true;
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Passwort zurücksetzen</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<h2>Neues Passwort setzen</h2>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php elseif ($success): ?>
<p class="login-error" style="color:#28a745;">
Passwort erfolgreich geändert. <a href="login.php">Jetzt einloggen</a>
</p>
<?php else: ?>
<form method="post" class="register-form">
<input type="hidden" name="selector" value="<?= htmlspecialchars($selector) ?>">
<input type="hidden" name="validator" value="<?= htmlspecialchars($validator) ?>">
<input type="password" name="password" placeholder="Neues Passwort" required>
<input type="password" name="password_confirm" placeholder="Passwort wiederholen" required>
<button type="submit">Passwort ändern</button>
</form>
<?php endif; ?>
</div>
</div>
</body>
</html>
+190
View File
@@ -0,0 +1,190 @@
<?php
if (!isset($selectedService)) {
exit('Direkter Zugriff nicht erlaubt.');
}
?>
<div>
<!-- Formular für INSERT -->
<div class="vergabe-form">
<h3>1. Eintrag hinzufügen (DatabaseInsert)</h3>
<form id="insertForm" class="vergabe-grid">
<div class="form-group">
<label>Artikelname</label>
<input type="text" id="insertProdukt" placeholder="z.B. Apfel" required>
</div>
<div class="form-group">
<label>Menge</label>
<input type="text" id="insertMenge" placeholder="z.B. 5 Stück" required>
</div>
<button type="submit" class="btn-submit-vergabe">💾 Hinzufügen</button>
</form>
</div>
<!-- Suchfeld für SHOW mit Filtern -->
<div class="form-container" style="margin-top: 20px;">
<h3>2. Liste filtern (DatabaseShow mit Conditions)</h3>
<div class="user-form-grid">
<div class="form-group">
<label>Suche nach Produkt (LIKE)</label>
<input type="text" id="searchProdukt" placeholder="Leer lassen für alle Einträge...">
</div>
<button type="button" id="btnFilter" class="btn btn-primary" style="margin: 0; width: 100%; height: 43px;">
🔍 Filtern
</button>
</div>
</div>
<!-- Tabellen-Container -->
<div class="table-container" style="margin-top: 30px;">
<h3>3 & 4. Daten verwalten (Tabelle via JS gerendert)</h3>
<div id="showcaseTableWrapper">
<p>Lade daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('showcaseTableWrapper');
const dbTable = 'Einkaufsliste';
const spaltenKonfig = [{
key: 'PRODUKT',
label: 'Produkt'
},
{
key: 'MENGE',
label: 'Menge'
}
];
// ALL IN ONE: Daten laden und via AjaxMethods-Tabelle anzeigen
async function loadData() {
const searchQuery = document.getElementById('searchProdukt').value.trim();
let conditions = [];
if (searchQuery !== '') {
conditions.push({
column: 'PRODUKT',
operator: 'LIKE',
value: `%${searchQuery}%`
});
}
try {
// 1. SHOW ausführen
const data = await DatabaseShow(dbTable, ['ID', 'PRODUKT', 'MENGE'], conditions);
// 2. TABELLE RENDERN(Nutzt die neue Methode aus AjaxMethods.js)
// renderTableWithEdit(
// tableWrapper,
// data.entries,
// spaltenKonfig,
// 'ID',
// async (id) => {
// await editWorkflow(id);
// }, // Edit Callback
// );
// renderTableWithDelete(
// tableWrapper,
// data.entries,
// spaltenKonfig,
// 'ID',
// async (id) => {
// await deleteWorkflow(id);
// }, // Edit Callback
// )
// renderTableWithEditAndDelete(
// tableWrapper,
// data.entries,
// spaltenKonfig,
// 'ID',
// async (id) => {
// await editWorkflow(id);
// }, // Edit Callback
// async (id) => {
// await deleteWorkflow(id);
// } // Delete Callback
// )
renderTable(
tableWrapper,
data.entries,
spaltenKonfig,
)
} catch (error) {
tableWrapper.innerHTML = `<p style="color:red;">❌ Fehler: ${escapeHtml(error.message)}</p>`;
}
}
// WORKFLOW: INSERT
document.getElementById('insertForm').addEventListener('submit', async (e) => {
e.preventDefault();
const produkt = document.getElementById('insertProdukt').value.trim();
const menge = document.getElementById('insertMenge').value.trim();
try {
await DatabaseInsert(dbTable, ['PRODUKT', 'MENGE'], [produkt, menge]);
e.target.reset();
await loadData();
} catch (error) {
alert('Fehler beim Einfügen: ' + error.message);
}
});
// WORKFLOW: UPDATE
async function editWorkflow(id) {
const neueMenge = prompt('Neue Menge (Abbrechen für keine Änderung):');
if (neueMenge === null) return; // Abgebrochen
// Daten-Objekt für den UPDATE-Query vorbereiten
let updates = {};
if (neueMenge.trim() !== '') updates.MENGE = neueMenge.trim();
// Falls beide Felder leer gelassen wurden, abbrechen
if (Object.keys(updates).length === 0) return;
try {
await DatabaseUpdate(
dbTable,
updates,
[{
column: 'ID',
operator: '=',
value: id
}]
);
await loadData();
} catch (error) {
alert('Fehler beim Aktualisieren: ' + error.message);
}
}
// WORKFLOW: DELETE
async function deleteWorkflow(id) {
try {
await DatabaseDelete(dbTable, [{
column: 'ID',
operator: '=',
value: id
}]);
await loadData();
} catch (error) {
alert('Fehler beim Löschen: ' + error.message);
}
}
// Event Listener für Filter-Button
document.getElementById('btnFilter').addEventListener('click', loadData);
// Erster App-Start
loadData();
});
</script>
+62 -75
View File
@@ -1,91 +1,78 @@
<?php
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/RoutesController.php';
$view = $_GET['view'] ?? 'show';
$selectedService = $_GET['service'] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_entry'])) {
$artikel = $_POST['artikel'];
$menge = $_POST['menge'];
DatabaseInsert(
"Einkaufsliste",
"PRODUKT, MENGE",
$artikel,
$menge
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_entry'])) {
$id = $_POST['id'];
DatabaseDelete("Einkaufsliste", "ID = ?", $id);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
$entries = DatabaseShow(
"Einkaufsliste"
);
?>
<div>
<div class="vergabe-form">
<form method="POST" class="vergabe-grid">
<form id="addForm" class="vergabe-grid">
<div class="form-group">
<label>Artikel</label>
<input type="text" name="artikel" required>
<input type="text" name="artikel" id="artikel" required>
</div>
<div class="form-group">
<label>Menge</label>
<input type="text" name="menge" required>
<input type="text" name="menge" id="menge" required>
</div>
<button type="submit" name="add_entry" class="btn-submit-vergabe">
💾 Speichern
<button type="submit" class="btn-submit-vergabe">
Speichern
</button>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>Einkaufsliste</h3>
<?php if (!empty($entries)): ?>
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Artikel</th>
<th>Menge</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry): ?>
<tr>
<td data-label="Artikel"><?= htmlspecialchars($entry['PRODUKT'] ?? '') ?></td>
<td data-label="Menge"><?= htmlspecialchars($entry['MENGE'] ?? '') ?></td>
<td data-label="Aktion">
<form method="POST" onsubmit="return confirm('Möchtest du diesen Eintrag wirklich löschen?');" style="margin: 0;">
<input type="hidden" name="id" value="<?= $entry['ID'] ?? '' ?>">
<button type="submit" name="delete_entry" class="btn-delete">
🗑️ Löschen
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>Keine Einträge vorhanden.</p>
<?php endif; ?>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const table = 'Einkaufsliste';
const spaltenKonfig = [{
key: 'PRODUKT',
label: 'Artikel'
},
{
key: 'MENGE',
label: 'Menge'
},
];
async function reloadTable() {
try {
const data = await DatabaseShow(table);
renderTableWithDelete(tableWrapper, data.entries, spaltenKonfig, 'ID', async (id) => {
await DatabaseDelete(table, [{
column: 'ID',
operator: '=',
value: id
}]);
await reloadTable();
});
} catch (error) {
tableWrapper.innerHTML = `<p> ${escapeHtml(error.message)}</p>`;
}
}
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const artikel = document.getElementById('artikel').value.trim();
const menge = document.getElementById('menge').value.trim();
if (!artikel || !menge) return;
try {
await DatabaseInsert(table, ['PRODUKT', 'MENGE'], [artikel, menge]);
e.target.reset();
await reloadTable();
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
});
reloadTable();
});
</script>
+159 -179
View File
@@ -1,108 +1,8 @@
<?php
include_once(__DIR__ . '/../controller/DatabaseController.php');
include_once(__DIR__ . '/../controller/DataController.php');
$msg = "";
$username = $_SESSION['username'] ?? 'Gast';
if (isset($_GET['action']) && $_GET['action'] === 'delete') {
if (isset($_GET['serien_id']) && !empty($_GET['serien_id'])) {
$serienId = $_GET['serien_id'];
DatabaseDelete("Termine", "SERIEN_ID = ?", $serienId);
$msg = "Komplette Terminserie erfolgreich gelöscht!";
} elseif (isset($_GET['id'])) {
$deleteId = (int)$_GET['id'];
DatabaseDelete("Termine", "ID = ?", $deleteId);
$msg = "Termin erfolgreich gelöscht!";
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_termin'])) {
$datum = $_POST['datum'] ?? '';
$zeit = $_POST['zeit'] ?? '';
$name = $_POST['name'] ?? '';
$notiz = $_POST['notiz'] ?? '';
$wiederholung = $_POST['wiederholung'] ?? 'none';
$enddatum = $_POST['enddatum'] ?? '';
if (!empty($datum) && !empty($zeit) && !empty($name)) {
if ($wiederholung !== 'none' && !empty($enddatum)) {
$currentDate = new DateTime($datum);
$endDate = new DateTime($enddatum);
if ($endDate < $currentDate) {
$msg = "Das Enddatum darf nicht vor dem Startdatum liegen.";
} else {
$insertedCount = 0;
$serienId = uniqid('serie_', true);
$intervalMapping = [
'daily' => '+1 day',
'weekly' => '+1 week',
'monthly' => '+1 month'
];
$modifier = $intervalMapping[$wiederholung] ?? null;
if ($modifier) {
while ($currentDate <= $endDate) {
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON, SERIEN_ID, INTERVALL, SERIEN_ENDE",
$currentDate->format('Y-m-d'),
$zeit,
$name,
$notiz,
$username,
$serienId,
$wiederholung,
$enddatum
);
$insertedCount++;
$currentDate->modify($modifier);
}
$msg = "Serientermin ($insertedCount Termine) erfolgreich eingetragen!";
}
}
} else {
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON",
$datum,
$zeit,
$name,
$notiz,
$username
);
$msg = "Termin erfolgreich eingetragen!";
}
} else {
$msg = "Bitte alle Pflichtfelder ausfüllen.";
}
}
$termine = DatabaseShow(
"Termine",
"MAX(ID) as ID, MIN(DATUM) as DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON, SERIEN_ID, INTERVALL, SERIEN_ENDE",
"ERSTELLT_VON = ? GROUP BY IFNULL(SERIEN_ID, ID) ORDER BY DATUM ASC, ZEIT ASC",
$username
);
$intervallNamen = [
'daily' => 'Täglich',
'weekly' => 'Wöchentlich',
'monthly' => 'Monatlich'
];
?>
<?php if (!empty($msg)): ?>
<div class="status-msg"><?= htmlspecialchars($msg) ?></div>
<?php endif; ?>
<div id="statusMsg" class="status-msg" style="display:none;"></div>
<div class="form-container">
<h3>📅 Neuen Termin vergeben</h3>
<form method="POST" class="vergabe-form">
<form id="addTerminForm" class="vergabe-form">
<div class="vergabe-grid">
<div class="form-group">
<label for="datum">Start-Datum *</label>
@@ -123,7 +23,7 @@ $intervallNamen = [
</div>
<div class="form-group">
<label for="wiederholung">Wiederholung</label>
<select id="wiederholung" name="wiederholung" onchange="toggleEnddatum()"
<select id="wiederholung" name="wiederholung"
style="width:100%; padding:10px; border-radius:5px; border:1px solid var(--input-border); background:var(--input-bg); color:var(--input-text);">
<option value="none">Keine Wiederholung</option>
<option value="daily">Täglich</option>
@@ -135,7 +35,7 @@ $intervallNamen = [
<label for="enddatum">Wiederholen bis *</label>
<input type="date" id="enddatum" name="enddatum">
</div>
<button type="submit" name="add_termin" class="btn-submit-vergabe">Speichern</button>
<button type="submit" class="btn-submit-vergabe">Speichern</button>
</div>
</form>
</div>
@@ -143,85 +43,165 @@ $intervallNamen = [
<div class="form-container">
<h3>📋 Aktuelle Terminbelegungen</h3>
<div class="table-container">
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Datum / Turnus</th>
<th>Uhrzeit</th>
<th>Termin / Name</th>
<th>Notiz</th>
<th>Eingetragen von</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php if (empty($termine)): ?>
<tr>
<td colspan="6" style="text-align: center; padding: 20px;">Keine Termine eingetragen.</td>
</tr>
<?php else: ?>
<?php foreach ($termine as $termin): ?>
<?php
$dateFormatted = date('d.m.Y', strtotime($termin['DATUM']));
$timeFormatted = date('H:i', strtotime($termin['ZEIT']));
$istSerie = !empty($termin['SERIEN_ID']);
?>
<tr>
<td data-label="Datum:">
<strong><?= htmlspecialchars($dateFormatted) ?></strong>
<?php if ($istSerie): ?>
<br>
<small style="color: var(--primary-color, #007bff); font-style: italic;">
(<?= $intervallNamen[$termin['INTERVALL']] ?? '' ?>
bis <?= date('d.m.Y', strtotime($termin['SERIEN_ENDE'])) ?>)
</small>
<?php endif; ?>
</td>
<td data-label="Uhrzeit:"><?= htmlspecialchars($timeFormatted) ?></td>
<td data-label="Termin / Name:">
<strong><?= htmlspecialchars($termin['NAME']) ?></strong>
<?php if ($istSerie): ?>
<span
style="font-size: 0.8em; background: #e1f5fe; color: #0288d1; padding: 2px 6px; border-radius: 10px; margin-left: 5px;">Wiederholend</span>
<?php endif; ?>
</td>
<td data-label="Notiz:"><?= htmlspecialchars($termin['NOTIZ'] ?: '-') ?></td>
<td data-label="Von:"><span class="badge"><?= htmlspecialchars($termin['ERSTELLT_VON']) ?></span>
</td>
<td data-label="Aktion:">
<?php if ($istSerie): ?>
<a href="?service=termine&action=delete&serien_id=<?= urlencode($termin['SERIEN_ID']) ?>"
onclick="return confirm('Möchtest du die GESAMTE Terminserie löschen?');">
<button class="btn-delete" style="background-color: #d32f2f;">löschen</button>
</a>
<?php else: ?>
<a href="?service=termine&action=delete&id=<?= $termin['ID'] ?>"
onclick="return confirm('Möchtest du diesen Termin wirklich löschen?');">
<button class="btn-delete">Löschen</button>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<div id="termineTableWrapper">Lade Daten...</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script src="/js/ajaxApi/TermineAjaxMethods.js"></script>
<script>
function toggleEnddatum() {
var wiederholung = document.getElementById('wiederholung').value;
var enddatumGroup = document.getElementById('enddatum-group');
var enddatumInput = document.getElementById('enddatum');
document.addEventListener('DOMContentLoaded', () => {
const statusMsg = document.getElementById('statusMsg');
const tableWrapper = document.getElementById('termineTableWrapper');
if (wiederholung !== 'none') {
enddatumGroup.style.display = 'block';
enddatumInput.required = true;
} else {
enddatumGroup.style.display = 'none';
enddatumInput.required = false;
enddatumInput.value = '';
const intervallNamen = {
daily: 'Täglich',
weekly: 'Wöchentlich',
monthly: 'Monatlich'
};
function zeigeStatus(text, istFehler = false) {
statusMsg.textContent = text;
statusMsg.style.display = 'block';
statusMsg.style.background = istFehler ? '#f8d7da' : '';
statusMsg.style.color = istFehler ? '#721c24' : '';
setTimeout(() => {
statusMsg.style.display = 'none';
}, 4000);
}
}
function formatDatum(iso) {
const d = new Date(iso);
return d.toLocaleDateString('de-DE');
}
function formatZeit(zeit) {
return zeit?.slice(0, 5) ?? '';
}
function renderTermine(termine) {
if (!termine || termine.length === 0) {
tableWrapper.innerHTML = `
<table class="db-table mobile-responsive-table">
<thead><tr><th>Datum / Turnus</th><th>Uhrzeit</th><th>Termin / Name</th><th>Notiz</th><th>Eingetragen von</th><th>Aktion</th></tr></thead>
<tbody><tr><td colspan="6" style="text-align:center; padding:20px;">Keine Termine eingetragen.</td></tr></tbody>
</table>`;
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr><th>Datum / Turnus</th><th>Uhrzeit</th><th>Termin / Name</th><th>Notiz</th><th>Eingetragen von</th><th>Aktion</th></tr>
</thead>
<tbody>
`;
for (const t of termine) {
const istSerie = !!t.SERIEN_ID;
html += `
<tr>
<td data-label="Datum:">
<strong>${escapeHtml(formatDatum(t.DATUM))}</strong>
${istSerie ? `<br><small style="color:#007bff; font-style:italic;">(${escapeHtml(intervallNamen[t.INTERVALL] ?? '')} bis ${escapeHtml(formatDatum(t.SERIEN_ENDE))})</small>` : ''}
</td>
<td data-label="Uhrzeit:">${escapeHtml(formatZeit(t.ZEIT))}</td>
<td data-label="Termin / Name:">
<strong>${escapeHtml(t.NAME)}</strong>
${istSerie ? `<span style="font-size:0.8em; background:#e1f5fe; color:#0288d1; padding:2px 6px; border-radius:10px; margin-left:5px;">Wiederholend</span>` : ''}
</td>
<td data-label="Notiz:">${escapeHtml(t.NOTIZ || '-')}</td>
<td data-label="Von:"><span class="badge">${escapeHtml(t.ERSTELLT_VON)}</span></td>
<td data-label="Aktion:">
${istSerie
? `<button type="button" class="btn-delete btn-delete-serie" data-serien-id="${escapeHtml(t.SERIEN_ID)}" style="background-color:#d32f2f;">löschen</button>`
: `<button type="button" class="btn-delete btn-delete-einzeln" data-id="${escapeHtml(t.ID)}">Löschen</button>`
}
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-delete-einzeln').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Termin wirklich löschen?')) return;
try {
const data = await deleteTermin(btn.dataset.id);
zeigeStatus(data.message);
await ladeTermine();
} catch (err) {
zeigeStatus('Fehler: ' + err.message, true);
}
});
});
tableWrapper.querySelectorAll('.btn-delete-serie').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du die GESAMTE Terminserie löschen?')) return;
try {
const data = await deleteTerminSerie(btn.dataset.serienId);
zeigeStatus(data.message);
await ladeTermine();
} catch (err) {
zeigeStatus('Fehler: ' + err.message, true);
}
});
});
}
async function ladeTermine() {
try {
const data = await getTermine();
renderTermine(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
// -----------------------------------------------------------
// Wiederholung: Enddatum-Feld ein-/ausblenden
// -----------------------------------------------------------
document.getElementById('wiederholung').addEventListener('change', function() {
const enddatumGroup = document.getElementById('enddatum-group');
const enddatumInput = document.getElementById('enddatum');
if (this.value !== 'none') {
enddatumGroup.style.display = 'block';
enddatumInput.required = true;
} else {
enddatumGroup.style.display = 'none';
enddatumInput.required = false;
enddatumInput.value = '';
}
});
// -----------------------------------------------------------
// Formular absenden
// -----------------------------------------------------------
document.getElementById('addTerminForm').addEventListener('submit', async (e) => {
e.preventDefault();
const datum = document.getElementById('datum').value;
const zeit = document.getElementById('zeit').value;
const name = document.getElementById('name').value.trim();
const notiz = document.getElementById('notiz').value.trim();
const wiederholung = document.getElementById('wiederholung').value;
const enddatum = document.getElementById('enddatum').value;
try {
const data = await addTermin(datum, zeit, name, notiz, wiederholung, enddatum);
zeigeStatus(data.message);
e.target.reset();
document.getElementById('enddatum-group').style.display = 'none';
await ladeTermine();
} catch (err) {
zeigeStatus('Fehler: ' + err.message, true);
}
});
ladeTermine();
});
</script>
+144
View File
@@ -0,0 +1,144 @@
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<h3>Neues Ticket anlegen</h3>
<form id="addTicketForm" class="vergabe-grid">
<div class="form-group">
<label>Titel</label>
<input type="text" name="titel" id="in-titel" required>
</div>
<div class="form-group">
<label>Beschreibung</label>
<input type="text" name="beschreibung" id="in-beschreibung" required>
</div>
<button type="submit" class="btn-submit-vergabe">💾 Speichern</button>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>Tickets</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/ajaxApi/TicketBoardAjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const POLL_INTERVAL_MS = 5000;
let lastStatusById = {};
// -----------------------------------------------------------
// Tabelle rendern (eigene Render-Funktion wg. Status-Highlight)
// -----------------------------------------------------------
function renderTicketsTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Tickets vorhanden.</p>';
lastStatusById = {};
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Titel</th><th>Beschreibung</th><th>Status</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
const changed = lastStatusById[e.ID] !== undefined &&
lastStatusById[e.ID] !== e.STATUS;
html += `
<tr data-id="${escapeHtml(e.ID)}" class="${changed ? 'status-row-changed' : ''}">
<td data-label="Titel">${escapeHtml(e.TITEL)}</td>
<td data-label="Beschreibung">${escapeHtml(e.BESCHREIBUNG)}</td>
<td data-label="Status"><span class="status-badge">${escapeHtml(e.STATUS)}</span></td>
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}">Löschen</button>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
const neueStatus = {};
entries.forEach(e => {
neueStatus[e.ID] = e.STATUS;
});
lastStatusById = neueStatus;
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du dieses Ticket wirklich löschen?')) return;
try {
await deleteTicketEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
async function ladeListe() {
try {
const data = await getTicketEntries();
renderTicketsTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
// -----------------------------------------------------------
// Ticket anlegen
// -----------------------------------------------------------
document.getElementById('addTicketForm').addEventListener('submit', async (e) => {
e.preventDefault();
const titel = document.getElementById('in-titel').value.trim();
const beschreibung = document.getElementById('in-beschreibung').value.trim();
if (!titel || !beschreibung) return;
try {
await addTicketEntry(titel, beschreibung);
e.target.reset();
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
// -----------------------------------------------------------
// Initialisierung + zyklische Aktualisierung
// -----------------------------------------------------------
ladeListe();
setInterval(ladeListe, POLL_INTERVAL_MS);
});
</script>
<style>
@keyframes statusChanged {
0% {
background-color: #fff3cd;
}
100% {
background-color: transparent;
}
}
.status-row-changed {
animation: statusChanged 2.5s ease-out;
}
</style>
+97
View File
@@ -0,0 +1,97 @@
<div class="arbeitsberichte-wrapper">
<div class="table-container" style="margin-top: 30px;">
<h3>Ticket Verwaltung</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script src="/js/ajaxApi/TicketControlAjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const allowedStatuses = ['OFFEN', 'DEPLOYED', 'ABGELEHNT', 'ANGENOMMEN'];
function renderTicketControTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Tickets vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Titel</th><th>Beschreibung</th><th>Status</th><th>Ersteller</th>
<th style="width: 220px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
const options = allowedStatuses.map(s =>
`<option value="${escapeHtml(s)}" ${e.STATUS === s ? 'selected' : ''}>${escapeHtml(s)}</option>`
).join('');
html += `
<tr data-id="${escapeHtml(e.ID)}">
<td data-label="Titel">${escapeHtml(e.TITEL)}</td>
<td data-label="Beschreibung">${escapeHtml(e.BESCHREIBUNG)}</td>
<td data-label="Status">${escapeHtml(e.STATUS)}</td>
<td data-label="Ersteller">${escapeHtml(e.ERSTELLER)}</td>
<td data-label="Aktion">
<div style="display:flex; flex-direction:column; gap:6px; align-items:stretch;">
<select class="status-select" data-id="${escapeHtml(e.ID)}">
${options}
</select>
<button type="button" class="btn-edit" data-id="${escapeHtml(e.ID)}">Speichern</button>
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}" style="margin-top:3pt;">Löschen</button>
</div>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
const select = tableWrapper.querySelector(
`.status-select[data-id="${id}"]`);
try {
await editTicketControEntry(id, select.value);
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
});
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du dieses Ticket wirklich löschen?')) return;
try {
await deleteTicketControEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
async function ladeListe() {
try {
const data = await getTicketControEntries();
renderTicketControTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
ladeListe();
});
</script>
+163 -178
View File
@@ -1,201 +1,191 @@
<?php
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/RoutesController.php';
include_once __DIR__ . '/../modules/ConsoleLog.php';
$conn = db_connect();
$view = $_GET['view'] ?? 'show';
$selectedService = $_GET['service'] ?? null;
// --- Suchfilter auslesen ---
$searchWer = $_GET['search_wer'] ?? '';
$searchWas = $_GET['search_was'] ?? '';
// --- LOGIK: Überstunden-Eintrag (Abbau) speichern ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_entry'])) {
$was = $_POST['was'];
$wer = $_POST['wer'];
$wann = $_POST['wann'];
$rueckgabe = $_POST['rueckgabe'];
DatabaseInsert(
"Vergabe",
"WAS, WER, WANN, RUECKGABE",
$was,
$wer,
$wann,
$rueckgabe
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_entry'])) {
$id = $_POST['id'];
ConsoleLog("$id");
DatabaseUpdate(
"Vergabe",
"STATUS = ?",
"ID = ?",
1,
$id
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
// --- Dynamic Query mit kombiniertem Suchfilter ---
$whereClause = "STATUS IS NULL";
if (!empty($searchWer)) {
$whereClause .= " AND WER LIKE '%" . $conn->real_escape_string($searchWer) . "%'";
}
if (!empty($searchWas)) {
$whereClause .= " AND WAS LIKE '%" . $conn->real_escape_string($searchWas) . "%'";
}
$entries = DatabaseShow(
"Vergabe",
"ID, WAS, WER, WANN, RUECKGABE",
$whereClause
);
$searchEntries = DatabaseShow(
"Vergabe",
);
$existingWas = [];
$existingWer = [];
foreach ($searchEntries as $entry) {
if (!empty($entry['WAS'])) $existingWas[$entry['WAS']] = true;
if (!empty($entry['WER'])) $existingWer[$entry['WER']] = true;
}
$existingWas = array_keys($existingWas);
$existingWer = array_keys($existingWer);
?>
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<form method="POST" class="vergabe-grid">
<form id="addForm" class="vergabe-grid">
<div class="form-group">
<label>Wann ausgeliehen</label>
<input type="date" name="wann" value="<?= date('Y-m-d') ?>" required>
<input type="date" name="wann" id="in-wann" required>
</div>
<div class="form-group">
<label>Rückgabe</label>
<input type="date" name="rueckgabe" value="<?= date('Y-m-d') ?>" required>
<input type="date" name="rueckgabe" id="in-rueckgabe" required>
</div>
<div class="form-group autocomplete-wrapper">
<label>An wen</label>
<input type="text" name="wer" id="wer-input" placeholder="Name eingeben" required autocomplete="off">
<input type="text" id="wer-input" placeholder="Name eingeben" required autocomplete="off">
<div class="autocomplete-suggestions" id="wer-suggestions"></div>
</div>
<div class="form-group autocomplete-wrapper">
<label>Was</label>
<input type="text" name="was" id="was-input" placeholder="Gegenstand eingeben" required autocomplete="off">
<input type="text" id="was-input" placeholder="Gegenstand eingeben" required autocomplete="off">
<div class="autocomplete-suggestions" id="was-suggestions"></div>
</div>
<button type="submit" name="add_entry" class="btn-submit-vergabe">
Speichern
</button>
<button type="submit" class="btn-submit-vergabe">Speichern</button>
</form>
</div>
<div class="vergabe-form" style="margin-top: 30px;">
<form method="GET" action="" class="vergabe-grid" id="search-form">
<input type="hidden" name="view" value="<?= htmlspecialchars($view) ?>">
<?php if ($selectedService): ?>
<input type="hidden" name="service" value="<?= htmlspecialchars($selectedService) ?>">
<?php endif; ?>
<form id="searchForm" class="vergabe-grid">
<div class="form-group autocomplete-wrapper">
<label for="search-wer-input">Nach Person filtern</label>
<input type="text" name="search_wer" id="search-wer-input" value="<?= htmlspecialchars($searchWer) ?>" placeholder="Name eingeben..." autocomplete="off">
<input type="text" id="search-wer-input" placeholder="Name eingeben..." autocomplete="off">
<div class="autocomplete-suggestions" id="search-wer-suggestions"></div>
</div>
<div class="form-group autocomplete-wrapper">
<label for="search-was-input">Nach Gegenstand filtern</label>
<input type="text" name="search_was" id="search-was-input" value="<?= htmlspecialchars($searchWas) ?>" placeholder="Gegenstand eingeben..." autocomplete="off">
<input type="text" id="search-was-input" placeholder="Gegenstand eingeben..." autocomplete="off">
<div class="autocomplete-suggestions" id="search-was-suggestions"></div>
</div>
<div style="display: flex; gap: 10px; align-items: flex-end;">
<button type="submit" class="btn-submit-vergabe" style="margin: 0; flex-grow: 1;">
Suchen
</button>
<?php if (!empty($searchWer) || !empty($searchWas)): ?>
<a href="?view=<?= urlencode($view) ?><?= $selectedService ? '&service=' . urlencode($selectedService) : '' ?>" class="btn-delete" style="text-decoration: none; padding: 10px; display: inline-block; text-align: center; white-space: nowrap; margin: 0;">
Zurücksetzen
</a>
<?php endif; ?>
<button type="submit" class="btn-submit-vergabe" style="margin: 0; flex-grow: 1;">Suchen</button>
<button type="button" id="resetSearch" class="btn-delete" style="margin: 0;">Zurücksetzen</button>
</div>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>
<?php
if (!empty($searchWer) && !empty($searchWas)) {
echo "Gefiltert nach: " . htmlspecialchars($searchWer) . " ➔ " . htmlspecialchars($searchWas);
} elseif (!empty($searchWer)) {
echo "Vergaben für: " . htmlspecialchars($searchWer);
} elseif (!empty($searchWas)) {
echo "Ausgeliehene Gegenstände: " . htmlspecialchars($searchWas);
} else {
echo "Aktuelle Vergaben";
}
?>
</h3>
<h3 id="tableHeading">Aktuelle Vergaben</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<?php if (!empty($entries)): ?>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/ajaxApi/VergabeAjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const tableHeading = document.getElementById('tableHeading');
let aktuellSearchWer = '';
let aktuellSearchWas = '';
// -----------------------------------------------------------
// Tabelle rendern (eigene Render-Funktion, da "Zurückgeben"
// eine andere Aktion ist als das generische Löschen)
// -----------------------------------------------------------
function renderVergabeTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Was</th>
<th>Wer</th>
<th>Wann ausgeliehen</th>
<th>Rückgabe</th>
<th>Was</th><th>Wer</th><th>Wann ausgeliehen</th><th>Rückgabe</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry): ?>
<tr>
<td data-label="Was"><?= htmlspecialchars($entry['WAS'] ?? '') ?></td>
<td data-label="Wer"><?= htmlspecialchars($entry['WER'] ?? '') ?></td>
<td data-label="Wann"><?= htmlspecialchars($entry['WANN'] ?? '') ?></td>
<td data-label="Rückgabe"><?= htmlspecialchars($entry['RUECKGABE'] ?? '') ?></td>
<td data-label="Aktion">
<form method="POST" onsubmit="return confirm('Möchtest du diesen Eintrag wirklich löschen?');" style="margin: 0;">
<input type="hidden" name="id" value="<?= $entry['ID'] ?? '' ?>">
<button type="submit" name="delete_entry" class="btn-delete">
Löschen
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>Keine Einträge vorhanden.</p>
<?php endif; ?>
</div>
</div>
`;
for (const e of entries) {
html += `
<tr data-id="${escapeHtml(e.ID)}">
<td data-label="Was">${escapeHtml(e.WAS)}</td>
<td data-label="Wer">${escapeHtml(e.WER)}</td>
<td data-label="Wann">${escapeHtml(e.WANN)}</td>
<td data-label="Rückgabe">${escapeHtml(e.RUECKGABE)}</td>
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}">Löschen</button>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
<script>
// Parameter `submitOnSelect` hinzugefügt (Standard: false)
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
try {
await returnVergabeEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
function aktualisiereUeberschrift() {
if (aktuellSearchWer && aktuellSearchWas) {
tableHeading.textContent = `Gefiltert nach: ${aktuellSearchWer} ➔ ${aktuellSearchWas}`;
} else if (aktuellSearchWer) {
tableHeading.textContent = `Vergaben für: ${aktuellSearchWer}`;
} else if (aktuellSearchWas) {
tableHeading.textContent = `Ausgeliehene Gegenstände: ${aktuellSearchWas}`;
} else {
tableHeading.textContent = 'Aktuelle Vergaben';
}
}
async function ladeListe() {
try {
const data = await getVergabeEntries(aktuellSearchWer, aktuellSearchWas);
aktualisiereUeberschrift();
renderVergabeTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
// -----------------------------------------------------------
// Eintrag hinzufügen
// -----------------------------------------------------------
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const wann = document.getElementById('in-wann').value;
const rueckgabe = document.getElementById('in-rueckgabe').value;
const wer = document.getElementById('wer-input').value.trim();
const was = document.getElementById('was-input').value.trim();
if (!wann || !rueckgabe || !wer || !was) return;
try {
await addVergabeEntry(was, wer, wann, rueckgabe);
e.target.reset();
document.getElementById('in-wann').value = new Date().toISOString().slice(0, 10);
document.getElementById('in-rueckgabe').value = new Date().toISOString().slice(0, 10);
await ladeVorschlaege(); // neue WAS/WER-Werte könnten dazugekommen sein
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
// -----------------------------------------------------------
// Suche
// -----------------------------------------------------------
document.getElementById('searchForm').addEventListener('submit', async (e) => {
e.preventDefault();
aktuellSearchWer = document.getElementById('search-wer-input').value.trim();
aktuellSearchWas = document.getElementById('search-was-input').value.trim();
await ladeListe();
});
document.getElementById('resetSearch').addEventListener('click', async () => {
aktuellSearchWer = '';
aktuellSearchWas = '';
document.getElementById('search-wer-input').value = '';
document.getElementById('search-was-input').value = '';
await ladeListe();
});
// -----------------------------------------------------------
// Autocomplete (unverändert zu deiner Logik, nur Datenquelle
// kommt jetzt per AJAX statt server-seitig eingebettet)
// -----------------------------------------------------------
function setupAutocomplete(inputId, suggestionsId, data, submitOnSelect = false) {
const input = document.getElementById(inputId);
const box = document.getElementById(suggestionsId);
@@ -225,27 +215,20 @@ $existingWer = array_keys($existingWer);
}
function updateActive() {
[...box.children].forEach((el, i) => {
el.classList.toggle('active', i === activeIndex);
[...box.children].forEach((el, i) => el.classList.toggle('active', i === activeIndex));
if (activeIndex >= 0) box.children[activeIndex].scrollIntoView({
block: 'nearest'
});
if (activeIndex >= 0) {
box.children[activeIndex].scrollIntoView({
block: 'nearest'
});
}
}
function selectItem(i) {
if (items[i] !== undefined) {
input.value = items[i];
}
if (items[i] !== undefined) input.value = items[i];
box.style.display = 'none';
activeIndex = -1;
// NEU: Wenn gewünscht, schicke das übergeordnete Formular sofort ab
if (submitOnSelect) {
const form = input.closest('form');
if (form) form.submit();
input.closest('form').dispatchEvent(new Event('submit', {
cancelable: true
}));
}
}
@@ -255,13 +238,11 @@ $existingWer = array_keys($existingWer);
box.style.display = 'none';
return;
}
const filtered = data.filter(d => d.toLowerCase().includes(val));
render(filtered);
render(data.filter(d => d.toLowerCase().includes(val)));
});
input.addEventListener('keydown', (e) => {
if (box.style.display !== 'block') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIndex = Math.min(activeIndex + 1, items.length - 1);
@@ -274,26 +255,30 @@ $existingWer = array_keys($existingWer);
if (activeIndex >= 0) {
e.preventDefault();
selectItem(activeIndex);
} else {
box.style.display = 'none';
}
} else if (e.key === 'Escape') {
box.style.display = 'none';
}
} else box.style.display = 'none';
} else if (e.key === 'Escape') box.style.display = 'none';
});
document.addEventListener('click', (e) => {
if (e.target !== input) {
box.style.display = 'none';
}
if (e.target !== input) box.style.display = 'none';
});
}
// 1. Normales Autocomplete für das Eintragungs-Formular (schickt nicht automatisch ab)
setupAutocomplete('was-input', 'was-suggestions', <?= json_encode($existingWas) ?>);
setupAutocomplete('wer-input', 'wer-suggestions', <?= json_encode($existingWer) ?>);
async function ladeVorschlaege() {
const data = await getVergabeSuggestions();
setupAutocomplete('was-input', 'was-suggestions', data.was);
setupAutocomplete('wer-input', 'wer-suggestions', data.wer);
setupAutocomplete('search-wer-input', 'search-wer-suggestions', data.wer, true);
setupAutocomplete('search-was-input', 'search-was-suggestions', data.was, true);
}
// 2. Such-Autocomplete: Hier wird als 4. Parameter `true` übergeben, damit sofort abgesendet wird!
setupAutocomplete('search-wer-input', 'search-wer-suggestions', <?= json_encode($existingWer) ?>, true);
setupAutocomplete('search-was-input', 'search-was-suggestions', <?= json_encode($existingWas) ?>, true);
// -----------------------------------------------------------
// Initialisierung
// -----------------------------------------------------------
document.getElementById('in-wann').value = new Date().toISOString().slice(0, 10);
document.getElementById('in-rueckgabe').value = new Date().toISOString().slice(0, 10);
ladeVorschlaege();
ladeListe();
});
</script>
+61 -57
View File
@@ -1,73 +1,27 @@
<?php
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/RoutesController.php';
$conn = db_connect();
$view = $_GET['view'] ?? 'show';
$selectedService = $_GET['service'];
// --- LOGIK: Überstunden-Eintrag (Abbau) speichern ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_overtime'])) {
$datum = $_POST['ot_date'];
$minuten = intval($_POST['ot_minutes']);
$grund = $conn->real_escape_string($_POST['ot_reason']);
DatabaseInsert(
"Ueberstunden",
"DATUM, MINUTEN, GRUND",
$datum,
$minuten,
$grund
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
// --- Gesamtsumme berechnen ---
$minuten_berichte = 0;
$res1 = $conn->query("SELECT SUM(OVERTIME) AS summe FROM Firmenberichte");
if ($row = $res1->fetch_object()) { $minuten_berichte = intval($row->summe); }
$minuten_abbau = 0;
$res2 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Abbau'");
if ($row = $res2->fetch_object()) { $minuten_abbau = intval($row->summe); }
$minuten_aufbau = 0;
$res2 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Aufbau'");
if ($row = $res2->fetch_object()) { $minuten_aufbau = intval($row->summe); }
$gesamt_minuten = $minuten_berichte - $minuten_abbau + $minuten_aufbau;
?>
<div class="arbeitsberichte-wrapper">
<div
<div id="overtimeCard"
style="background: rgba(150,150,150,0.05); padding: 25px; border-radius: 10px; margin-bottom: 25px; border: 1px solid rgba(150,150,150,0.2); border-left: 6px solid #007bff;">
<div style="margin-bottom: 20px;">
<span style="color: #888; font-size: 0.8rem; font-weight: bold; text-transform: uppercase;">Aktueller
Stand</span>
<h2 style="margin: 5px 0 0 0; font-size: 2rem; color: <?= $gesamt_minuten >= 0 ? '#28a745' : '#dc3545' ?>;">
<?= intdiv($gesamt_minuten, 60) ?>h <?= abs($gesamt_minuten % 60) ?>min
</h2>
<h2 id="standAnzeige" style="margin: 5px 0 0 0; font-size: 2rem;">Lädt...</h2>
</div>
<?php if ($_SESSION['username'] == "bruno" || $_SESSION['username'] == "Bruno"): ?>
<form method="POST" style="display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end;">
<input type="hidden" name="add_overtime" value="1">
<form id="overtimeForm" style="display: none; gap: 15px; flex-wrap: wrap; align-items: flex-end;">
<div style="display: flex; flex-direction: column; gap: 5px;">
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Datum</label>
<input type="date" name="ot_date" value="<?= date('Y-m-d') ?>" required
<input type="date" id="ot-date" required
style="padding: 8px; border: 1px solid rgba(150,150,150,0.3); border-radius: 4px; background: rgba(150,150,150,0.1); color: inherit;">
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Minuten Abbau</label>
<input type="number" name="ot_minutes" placeholder="480" required
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Minuten</label>
<input type="number" id="ot-minutes" placeholder="480" required
style="width: 100px; padding: 8px; border: 1px solid rgba(150,150,150,0.3); border-radius: 4px; background: rgba(150,150,150,0.1); color: inherit;">
</div>
<div class="form-group">
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Grund</label>
<select name="ot_reason" required>
<select id="ot-reason" required>
<option value="">Bitte wählen</option>
<option value="Abbau">Abbau</option>
<option value="Aufbau">Aufbau</option>
@@ -79,6 +33,56 @@ $gesamt_minuten = $minuten_berichte - $minuten_abbau + $minuten_aufbau;
Eintrag speichern
</button>
</form>
<?php endif ?>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script src="/js/ajaxApi/ZeiterfassungAjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const standAnzeige = document.getElementById('standAnzeige');
const overtimeForm = document.getElementById('overtimeForm');
function formatStand(gesamtMinuten) {
const stunden = Math.trunc(gesamtMinuten / 60);
const minuten = Math.abs(gesamtMinuten % 60);
return `${stunden}h ${minuten}min`;
}
async function ladeStand() {
try {
const data = await getOvertimeStats();
standAnzeige.textContent = formatStand(data.gesamtMinuten);
standAnzeige.style.color = data.gesamtMinuten >= 0 ? '#28a745' : '#dc3545';
overtimeForm.style.display = data.istBerechtigt ? 'flex' : 'none';
} catch (err) {
standAnzeige.textContent = 'Fehler beim Laden';
console.error(err.message);
}
}
overtimeForm.addEventListener('submit', async (e) => {
e.preventDefault();
const datum = document.getElementById('ot-date').value;
const minuten = document.getElementById('ot-minutes').value;
const grund = document.getElementById('ot-reason').value;
try {
await addOvertimeEntry(datum, minuten, grund);
e.target.reset();
document.getElementById('ot-date').value = new Date().toISOString().slice(0, 10);
await ladeStand();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
document.getElementById('ot-date').value = new Date().toISOString().slice(0, 10);
ladeStand();
});
</script>
+30 -6
View File
@@ -880,6 +880,17 @@ body.dark-mode .user-table .btn-primary:hover {
font-size: 0.8rem;
}
.btn-edit {
background-color: #32b844;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
/* ===== Toggle Switch ===== */
.toggle-card {
@@ -981,8 +992,7 @@ body.dark-mode .info-value {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
/* table-layout: fixed; */
/* sorgt für feste Spaltenbreite */
table-layout: fixed;
}
.db-table thead {
@@ -996,10 +1006,13 @@ body.dark-mode .info-value {
text-align: left;
border-bottom: 1px solid var(--card-border);
/* Wichtig für gleiche Größe */
/* overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; */
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
}
.db-table td:nth-child(2) {
max-width: 300px;
}
.db-table tr:hover {
@@ -1436,4 +1449,15 @@ body.dark-mode .berichte-form input:focus {
body.dark-mode .autocomplete-suggestions div:hover,
body.dark-mode .autocomplete-suggestions div.active {
background: rgba(0, 123, 255, 0.25);
}
.register-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.register-form input {
margin-bottom: 0;
/* gap übernimmt jetzt den Abstand, kein doppeltes Spacing mehr */
}
-22
View File
@@ -1,22 +0,0 @@
<?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 ComposerAutoloaderInit77f94e6fc6e485edf380978f04d2c8cd::getLoader();
-579
View File
@@ -1,579 +0,0 @@
<?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
View File
@@ -1,396 +0,0 @@
<?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;
}
}
-21
View File
@@ -1,21 +0,0 @@
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
View File
@@ -1,10 +0,0 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);
-9
View File
@@ -1,9 +0,0 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);
-11
View File
@@ -1,11 +0,0 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'),
'Hydrax\\Webroot\\' => array($baseDir . '/src'),
);
-38
View File
@@ -1,38 +0,0 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit77f94e6fc6e485edf380978f04d2c8cd
{
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('ComposerAutoloaderInit77f94e6fc6e485edf380978f04d2c8cd', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit77f94e6fc6e485edf380978f04d2c8cd', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit77f94e6fc6e485edf380978f04d2c8cd::getInitializer($loader));
$loader->register(true);
return $loader;
}
}
-44
View File
@@ -1,44 +0,0 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit77f94e6fc6e485edf380978f04d2c8cd
{
public static $prefixLengthsPsr4 = array (
'M' =>
array (
'MaxMind\\Db\\' => 11,
),
'H' =>
array (
'Hydrax\\Webroot\\' => 15,
),
);
public static $prefixDirsPsr4 = array (
'MaxMind\\Db\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db',
),
'Hydrax\\Webroot\\' =>
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 = ComposerStaticInit77f94e6fc6e485edf380978f04d2c8cd::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit77f94e6fc6e485edf380978f04d2c8cd::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit77f94e6fc6e485edf380978f04d2c8cd::$classMap;
}, null, ClassLoader::class);
}
}
-73
View File
@@ -1,73 +0,0 @@
{
"packages": [
{
"name": "maxmind-db/reader",
"version": "v1.13.1",
"version_normalized": "1.13.1.0",
"source": {
"type": "git",
"url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
"reference": "2194f58d0f024ce923e685cdf92af3daf9951908"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/2194f58d0f024ce923e685cdf92af3daf9951908",
"reference": "2194f58d0f024ce923e685cdf92af3daf9951908",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"conflict": {
"ext-maxminddb": "<1.11.1 || >=2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.*",
"phpstan/phpstan": "*",
"phpunit/phpunit": ">=8.0.0,<10.0.0",
"squizlabs/php_codesniffer": "4.*"
},
"suggest": {
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups",
"maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
},
"time": "2025-11-21T22:24:26+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"MaxMind\\Db\\": "src/MaxMind/Db"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Gregory J. Oschwald",
"email": "goschwald@maxmind.com",
"homepage": "https://www.maxmind.com/"
}
],
"description": "MaxMind DB Reader API",
"homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
"keywords": [
"database",
"geoip",
"geoip2",
"geolocation",
"maxmind"
],
"support": {
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.13.1"
},
"install-path": "../maxmind-db/reader"
}
],
"dev": true,
"dev-package-names": []
}
-32
View File
@@ -1,32 +0,0 @@
<?php return array(
'root' => array(
'name' => 'hydrax/webroot',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '75802de1424185c86220a17a734d7a4fa457b5f2',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'hydrax/webroot' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '75802de1424185c86220a17a734d7a4fa457b5f2',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'maxmind-db/reader' => array(
'pretty_version' => 'v1.13.1',
'version' => '1.13.1.0',
'reference' => '2194f58d0f024ce923e685cdf92af3daf9951908',
'type' => 'library',
'install_path' => __DIR__ . '/../maxmind-db/reader',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
-25
View File
@@ -1,25 +0,0 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.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)
);
}
-261
View File
@@ -1,261 +0,0 @@
CHANGELOG
=========
1.13.1 (2025-11-21)
-------------------
* First PIE release. No other changes.
1.13.0 (2025-11-20)
-------------------
* A redundant `filesize()` call in the reader's constructor was removed.
Pull request by Pavel Djundik. GitHub #189.
1.12.1 (2025-05-05)
-------------------
* The C extension now checks that the database metadata lookup was
successful.
1.12.0 (2024-11-14)
-------------------
* Improve the error handling when the user tries to open a directory
with the pure PHP reader.
* Improve the typehints on arrays in the PHPDocs.
1.11.1 (2023-12-01)
-------------------
* Resolve warnings when compiling the C extension.
* Fix various type issues detected by PHPStan level. Pull request by
LauraTaylorUK. GitHub #160.
1.11.0 (2021-10-18)
-------------------
* Replace runtime define of a constant to facilitate opcache preloading.
Reported by vedadkajtaz. GitHub #134.
* Resolve minor issue found by the Clang static analyzer in the C
extension.
1.10.1 (2021-04-14)
-------------------
* Fix a `TypeError` exception in the pure PHP reader when using large
databases on 32-bit PHP builds with the `bcmath` extension. Reported
by dodo1708. GitHub #124.
1.10.0 (2021-02-09)
-------------------
* When using the pure PHP reader, unsigned integers up to PHP_MAX_INT
will now be integers in PHP rather than strings. Previously integers
greater than 2^24 on 32-bit platforms and 2^56 on 64-bit platforms
would be strings due to the use of `gmp` or `bcmath` to decode them.
Reported by Alejandro Celaya. GitHub #119.
1.9.0 (2021-01-07)
------------------
* The `maxminddb` extension is now buildable on Windows. Pull request
by Jan Ehrhardt. GitHub #115.
1.8.0 (2020-10-01)
------------------
* Fixes for PHP 8.0. Pull Request by Remi Collet. GitHub #108.
1.7.0 (2020-08-07)
------------------
* IMPORTANT: PHP 7.2 or greater is now required.
* The extension no longer depends on the pure PHP classes in
`maxmind-db/reader`. You can use it independently.
* Type hints have been added to both the pure PHP implementation
and the extension.
* The `metadata` method on the reader now returns a new copy of the
metadata object rather than the actual object used by the reader.
* Work around PHP `is_readable()` bug. Reported by Ben Roberts. GitHub
#92.
* This is the first release of the extension as a PECL package.
GitHub #34.
1.6.0 (2019-12-19)
------------------
* 1.5.0 and 1.5.1 contained a possible memory corruptions when using
`getWithPrefixLen`. This has been fixed. Reported by proton-ab.
GitHub #96.
* The `composer.json` file now conflicts with all versions of the
`maxminddb` C extension less than the Composer version. This is to
reduce the chance of having an older, conflicting version of the
extension installed. You will need to upgrade the extension before
running `composer update`. Pull request by Benoît Burnichon. GitHub
#97.
1.5.1 (2019-12-12)
------------------
* Minor performance improvements.
* Make tests pass with older versions of libmaxminddb. PR by Remi
Collet. GitHub #90.
* Test enhancements. PR by Chun-Sheng, Li. GitHub #91.
1.5.0 (2019-09-30)
------------------
* PHP 5.6 or greater is now required.
* The C extension now supports PHP 8. Pull request by John Boehr.
GitHub #87.
* A new method, `getWithPrefixLen`, was added to the `Reader` class.
This method returns an array containing the record and the prefix
length for that record. GitHub #89.
1.4.1 (2019-01-04)
------------------
* The `maxminddb` extension now returns a string when a `uint32`
value is greater than `LONG_MAX`. Previously, the value would
overflow. This generally only affects 32-bit machines. Reported
by Remi Collet. GitHub #79.
* For `uint64` values, the `maxminddb` extension now returns an
integer rather than a string when the value is less than or equal
to `LONG_MAX`. This more closely matches the behavior of the pure
PHP reader.
1.4.0 (2018-11-20)
------------------
* The `maxminddb` extension now has the arginfo when using reflection.
PR by Remi Collet. GitHub #75.
* The `maxminddb` extension now provides `MINFO()` function that
displays the extension version and the libmaxminddb version. PR by
Remi Collet. GitHub #74.
* The `maxminddb` `configure` script now uses `pkg-config` when
available to get libmaxmindb build info. PR by Remi Collet.
GitHub #73.
* The pure PHP reader now correctly decodes integers on 32-bit platforms.
Previously, large integers would overflow. Reported by Remi Collet.
GitHub #77.
* There are small performance improvements for the pure PHP reader.
1.3.0 (2018-02-21)
------------------
* IMPORTANT: The `maxminddb` extension now obeys `open_basedir`. If
`open_basedir` is set, you _must_ store the database within the
specified directory. Placing the file outside of this directory
will result in an exception. Please test your integration before
upgrading the extension. This does not affect the pure PHP
implementation, which has always had this restriction. Reported
by Benoît Burnichon. GitHub #61.
* A custom `autoload.php` file is provided for installations without
Composer. GitHub #56.
1.2.0 (2017-10-27)
------------------
* PHP 5.4 or greater is now required.
* The `Reader` class for the `maxminddb` extension is no longer final.
This was change to match the behavior of the pure PHP class.
Reported and fixed by venyii. GitHub #52 & #54.
1.1.3 (2017-01-19)
------------------
* Fix incorrect version in `ext/php_maxminddb.h`. GitHub #48.
1.1.2 (2016-11-22)
------------------
* Searching for database metadata only occurs within the last 128KB
(128 * 1024 bytes) of the file, speeding detection of corrupt
datafiles. Reported by Eric Teubert. GitHub #42.
* Suggest relevant extensions when installing with Composer. GitHub #37.
1.1.1 (2016-09-15)
------------------
* Development files were added to the `.gitattributes` as `export-ignore` so
that they are not part of the Composer release. Pull request by Michele
Locati. GitHub #39.
1.1.0 (2016-01-04)
------------------
* The MaxMind DB extension now supports PHP 7. Pull request by John Boehr.
GitHub #27.
1.0.3 (2015-03-13)
------------------
* All uses of `strlen` were removed. This should prevent issues in situations
where the function is overloaded or otherwise broken.
1.0.2 (2015-01-19)
------------------
* Previously the MaxMind DB extension would cause a segfault if the Reader
object's destructor was called without first having called the constructor.
(Reported by Matthias Saou & Juan Peri. GitHub #20.)
1.0.1 (2015-01-12)
------------------
* In the last several releases, the version number in the extension was
incorrect. This release is being done to correct it. No other code changes
are included.
1.0.0 (2014-09-22)
------------------
* First production release.
* In the pure PHP reader, a string length test after `fread()` was replaced
with the difference between the start pointer and the end pointer. This
provided a 15% speed increase.
0.3.3 (2014-09-15)
------------------
* Clarified behavior of 128-bit type in documentation.
* Updated phpunit and fixed some test breakage from the newer version.
0.3.2 (2014-09-10)
------------------
* Fixed invalid reference to global class RuntimeException from namespaced
code. Fixed by Steven Don. GitHub issue #15.
* Additional documentation of `Metadata` class as well as misc. documentation
cleanup.
0.3.1 (2014-05-01)
------------------
* The API now works when `mbstring.func_overload` is set.
* BCMath is no longer required. If the decoder encounters a big integer,
it will try to use GMP and then BCMath. If both of those fail, it will
throw an exception. No databases released by MaxMind currently use big
integers.
* The API now officially supports HHVM when using the pure PHP reader.
0.3.0 (2014-02-19)
------------------
* This API is now licensed under the Apache License, Version 2.0.
* The code for the C extension was cleaned up, fixing several potential
issues.
0.2.0 (2013-10-21)
------------------
* Added optional C extension for using libmaxminddb in place of the pure PHP
reader.
* Significantly improved error handling in pure PHP reader.
* Improved performance for IPv4 lookups in an IPv6 database.
0.1.0 (2013-07-16)
------------------
* Initial release
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-214
View File
@@ -1,214 +0,0 @@
# MaxMind DB Reader PHP API #
## Description ##
This is the PHP API for reading MaxMind DB files. MaxMind DB is a binary file
format that stores data indexed by IP address subnets (IPv4 or IPv6).
## Installation ##
### C Extension (Recommended for Performance) ###
For significantly faster IP lookups, we recommend installing the C extension via
[PIE](https://github.com/php/pie):
```bash
pie install maxmind-db/reader-ext
```
The C extension requires the [libmaxminddb](https://github.com/maxmind/libmaxminddb)
C library. See the [installation instructions](https://github.com/maxmind/MaxMind-DB-Reader-php-ext#prerequisites)
for your platform.
### Pure PHP (No Compilation Required) ###
If you prefer not to compile a C extension or need maximum portability, you can
install the pure PHP implementation with [Composer](https://getcomposer.org/).
### Download Composer ###
To download Composer, run in the root directory of your project:
```bash
curl -sS https://getcomposer.org/installer | php
```
You should now have the file `composer.phar` in your project directory.
### Install Dependencies ###
Run in your project root:
```
php composer.phar require maxmind-db/reader:^1.13.1
```
You should now have the files `composer.json` and `composer.lock` as well as
the directory `vendor` in your project directory. If you use a version control
system, `composer.json` should be added to it.
### Require Autoloader ###
After installing the dependencies, you need to require the Composer autoloader
from your code:
```php
require 'vendor/autoload.php';
```
## Installation (Standalone) ##
If you don't want to use Composer for some reason, a custom
`autoload.php` is provided for you in the project root. To use the
library, simply include that file,
```php
require('/path/to/MaxMind-DB-Reader-php/autoload.php');
```
and then instantiate the reader class normally:
```php
use MaxMind\Db\Reader;
$reader = new Reader('example.mmdb');
```
## Installation (RPM)
RPMs are available in the [official Fedora repository](https://apps.fedoraproject.org/packages/php-maxminddb).
To install on Fedora, run:
```bash
dnf install php-maxminddb
```
To install on CentOS or RHEL 7, first [enable the EPEL repository](https://fedoraproject.org/wiki/EPEL)
and then run:
```bash
yum install php-maxminddb
```
Please note that these packages are *not* maintained by MaxMind.
## Usage ##
## Example ##
```php
<?php
require_once 'vendor/autoload.php';
use MaxMind\Db\Reader;
$ipAddress = '24.24.24.24';
$databaseFile = 'GeoIP2-City.mmdb';
$reader = new Reader($databaseFile);
// get returns just the record for the IP address
print_r($reader->get($ipAddress));
// getWithPrefixLen returns an array containing the record and the
// associated prefix length for that record.
print_r($reader->getWithPrefixLen($ipAddress));
$reader->close();
```
## Optional PHP C Extension ##
MaxMind provides an optional C extension that is a drop-in replacement for
`MaxMind\Db\Reader`. In order to use this extension, you must install the
Reader API as described above and install the extension as described below. If
you are using an autoloader, no changes to your code should be necessary.
### Installing Extension via PIE (Recommended) ###
We recommend installing the extension via [PIE](https://github.com/php/pie):
```bash
pie install maxmind-db/reader-ext
```
See the [extension repository](https://github.com/maxmind/MaxMind-DB-Reader-php-ext#prerequisites)
for prerequisites including libmaxminddb installation instructions.
### Installing Extension via PECL (Legacy) ###
First install [libmaxminddb](https://github.com/maxmind/libmaxminddb) as
described in its [README.md
file](https://github.com/maxmind/libmaxminddb/blob/main/README.md#installing-from-a-tarball).
After successfully installing libmaxmindb, you may install the extension
from [PECL](https://pecl.php.net/package/maxminddb):
```
pecl install maxminddb
```
### Installing Extension from Source ###
Alternatively, you may install it from the source. To do so, run the following
commands from the top-level directory of this distribution:
```
cd ext
phpize
./configure
make
make test
sudo make install
```
You then must load your extension. The recommended method is to add the
following to your `php.ini` file:
```
extension=maxminddb.so
```
Note: You may need to install the PHP development package on your OS such as
php5-dev for Debian-based systems or php-devel for RedHat/Fedora-based ones.
## 128-bit Integer Support ##
The MaxMind DB format includes 128-bit unsigned integer as a type. Although
no MaxMind-distributed database currently makes use of this type, both the
pure PHP reader and the C extension support this type. The pure PHP reader
requires gmp or bcmath to read databases with 128-bit unsigned integers.
The integer is currently returned as a hexadecimal string (prefixed with "0x")
by the C extension and a decimal string (no prefix) by the pure PHP reader.
Any change to make the reader implementations always return either a
hexadecimal or decimal representation of the integer will NOT be considered a
breaking change.
## Support ##
Please report all issues with this code using the [GitHub issue tracker](https://github.com/maxmind/MaxMind-DB-Reader-php/issues).
If you are having an issue with a MaxMind service that is not specific to the
client API, please see [our support page](https://www.maxmind.com/en/support).
## Requirements ##
This library requires PHP 7.2 or greater.
The GMP or BCMath extension may be required to read some databases
using the pure PHP API.
## Contributing ##
Patches and pull requests are encouraged. All code should follow the PSR-1 and
PSR-2 style guidelines. Please include unit tests whenever possible.
## Versioning ##
The MaxMind DB Reader PHP API uses [Semantic Versioning](https://semver.org/).
## Copyright and License ##
This software is Copyright (c) 2014-2025 by MaxMind, Inc.
This is free software, licensed under the Apache License, Version 2.0.
-47
View File
@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
/**
* PSR-4 autoloader implementation for the MaxMind\DB namespace.
* First we define the 'mmdb_autoload' function, and then we register
* it with 'spl_autoload_register' so that PHP knows to use it.
*
* @param mixed $class
*/
/**
* Automatically include the file that defines <code>class</code>.
*
* @param string $class
* the name of the class to load
*/
function mmdb_autoload($class): void
{
/*
* A project-specific mapping between the namespaces and where
* they're located. By convention, we include the trailing
* slashes. The one-element array here simply makes things easy
* to extend in the future if (for example) the test classes
* begin to use one another.
*/
$namespace_map = ['MaxMind\Db\\' => __DIR__ . '/src/MaxMind/Db/'];
foreach ($namespace_map as $prefix => $dir) {
// First swap out the namespace prefix with a directory...
$path = str_replace($prefix, $dir, $class);
// replace the namespace separator with a directory separator...
$path = str_replace('\\', '/', $path);
// and finally, add the PHP file extension to the result.
$path .= '.php';
// $path should now contain the path to a PHP file defining $class
if (file_exists($path)) {
include $path;
}
}
}
spl_autoload_register('mmdb_autoload');
-43
View File
@@ -1,43 +0,0 @@
{
"name": "maxmind-db/reader",
"description": "MaxMind DB Reader API",
"keywords": ["database", "geoip", "geoip2", "geolocation", "maxmind"],
"homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
"type": "library",
"license": "Apache-2.0",
"authors": [
{
"name": "Gregory J. Oschwald",
"email": "goschwald@maxmind.com",
"homepage": "https://www.maxmind.com/"
}
],
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups",
"maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
},
"conflict": {
"ext-maxminddb": "<1.11.1 || >=2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.*",
"phpunit/phpunit": ">=8.0.0,<10.0.0",
"squizlabs/php_codesniffer": "4.*",
"phpstan/phpstan": "*"
},
"autoload": {
"psr-4": {
"MaxMind\\Db\\": "src/MaxMind/Db"
}
},
"autoload-dev": {
"psr-4": {
"MaxMind\\Db\\Test\\Reader\\": "tests/MaxMind/Db/Test/Reader"
}
}
}
-40
View File
@@ -1,40 +0,0 @@
PHP_ARG_WITH(maxminddb,
[Whether to enable the MaxMind DB Reader extension],
[ --with-maxminddb Enable MaxMind DB Reader extension support])
PHP_ARG_ENABLE(maxminddb-debug, for MaxMind DB debug support,
[ --enable-maxminddb-debug Enable MaxMind DB debug support], no, no)
if test $PHP_MAXMINDDB != "no"; then
AC_PATH_PROG(PKG_CONFIG, pkg-config, no)
AC_MSG_CHECKING(for libmaxminddb)
if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libmaxminddb; then
dnl retrieve build options from pkg-config
if $PKG_CONFIG libmaxminddb --atleast-version 1.0.0; then
LIBMAXMINDDB_INC=`$PKG_CONFIG libmaxminddb --cflags`
LIBMAXMINDDB_LIB=`$PKG_CONFIG libmaxminddb --libs`
LIBMAXMINDDB_VER=`$PKG_CONFIG libmaxminddb --modversion`
AC_MSG_RESULT(found version $LIBMAXMINDDB_VER)
else
AC_MSG_ERROR(system libmaxminddb must be upgraded to version >= 1.0.0)
fi
PHP_EVAL_LIBLINE($LIBMAXMINDDB_LIB, MAXMINDDB_SHARED_LIBADD)
PHP_EVAL_INCLINE($LIBMAXMINDDB_INC)
else
AC_MSG_RESULT(pkg-config information missing)
AC_MSG_WARN(will use libmaxmxinddb from compiler default path)
PHP_CHECK_LIBRARY(maxminddb, MMDB_open)
PHP_ADD_LIBRARY(maxminddb, 1, MAXMINDDB_SHARED_LIBADD)
fi
if test $PHP_MAXMINDDB_DEBUG != "no"; then
CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror"
fi
PHP_SUBST(MAXMINDDB_SHARED_LIBADD)
PHP_NEW_EXTENSION(maxminddb, maxminddb.c, $ext_shared)
fi
-10
View File
@@ -1,10 +0,0 @@
ARG_WITH("maxminddb", "Enable MaxMind DB Reader extension support", "no");
if (PHP_MAXMINDDB == "yes") {
if (CHECK_HEADER_ADD_INCLUDE("maxminddb.h", "CFLAGS_MAXMINDDB", PHP_MAXMINDDB + ";" + PHP_PHP_BUILD + "\\include\\maxminddb") &&
CHECK_LIB("libmaxminddb.lib", "maxminddb", PHP_MAXMINDDB)) {
EXTENSION("maxminddb", "maxminddb.c");
} else {
WARNING('Could not find maxminddb.h or libmaxminddb.lib; skipping');
}
}
-819
View File
@@ -1,819 +0,0 @@
/* MaxMind, Inc., licenses this file to you under the Apache License, Version
* 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
#include "php_maxminddb.h"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <php.h>
#include <zend.h>
#include "Zend/zend_exceptions.h"
#include "Zend/zend_types.h"
#include "ext/spl/spl_exceptions.h"
#include "ext/standard/info.h"
#include <maxminddb.h>
#ifdef ZTS
#include <TSRM.h>
#endif
#define __STDC_FORMAT_MACROS
#include <inttypes.h>
#define PHP_MAXMINDDB_NS ZEND_NS_NAME("MaxMind", "Db")
#define PHP_MAXMINDDB_READER_NS ZEND_NS_NAME(PHP_MAXMINDDB_NS, "Reader")
#define PHP_MAXMINDDB_METADATA_NS \
ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "Metadata")
#define PHP_MAXMINDDB_READER_EX_NS \
ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "InvalidDatabaseException")
#define Z_MAXMINDDB_P(zv) php_maxminddb_fetch_object(Z_OBJ_P(zv))
typedef size_t strsize_t;
typedef zend_object free_obj_t;
/* For PHP 8 compatibility */
#if PHP_VERSION_ID < 80000
#define PROP_OBJ(zv) (zv)
#else
#define PROP_OBJ(zv) Z_OBJ_P(zv)
#define TSRMLS_C
#define TSRMLS_CC
#define TSRMLS_DC
/* End PHP 8 compatibility */
#endif
#ifndef ZEND_ACC_CTOR
#define ZEND_ACC_CTOR 0
#endif
/* IS_MIXED was added in 2020 */
#ifndef IS_MIXED
#define IS_MIXED IS_UNDEF
#endif
/* ZEND_THIS was added in 7.4 */
#ifndef ZEND_THIS
#define ZEND_THIS (&EX(This))
#endif
typedef struct _maxminddb_obj {
MMDB_s *mmdb;
zend_object std;
} maxminddb_obj;
PHP_FUNCTION(maxminddb);
static int
get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len);
static const MMDB_entry_data_list_s *
handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static const MMDB_entry_data_list_s *
handle_array(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static const MMDB_entry_data_list_s *
handle_map(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
#define CHECK_ALLOCATED(val) \
if (!val) { \
zend_error(E_ERROR, "Out of memory"); \
return; \
}
static zend_object_handlers maxminddb_obj_handlers;
static zend_class_entry *maxminddb_ce, *maxminddb_exception_ce, *metadata_ce;
static inline maxminddb_obj *
php_maxminddb_fetch_object(zend_object *obj TSRMLS_DC) {
return (maxminddb_obj *)((char *)(obj)-XtOffsetOf(maxminddb_obj, std));
}
ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, db_file, IS_STRING, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, __construct) {
char *db_file = NULL;
strsize_t name_len;
zval *_this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"Os",
&_this_zval,
maxminddb_ce,
&db_file,
&name_len) == FAILURE) {
return;
}
if (0 != php_check_open_basedir(db_file TSRMLS_CC) ||
0 != access(db_file, R_OK)) {
zend_throw_exception_ex(
spl_ce_InvalidArgumentException,
0 TSRMLS_CC,
"The file \"%s\" does not exist or is not readable.",
db_file);
return;
}
MMDB_s *mmdb = (MMDB_s *)ecalloc(1, sizeof(MMDB_s));
int const status = MMDB_open(db_file, MMDB_MODE_MMAP, mmdb);
if (MMDB_SUCCESS != status) {
zend_throw_exception_ex(
maxminddb_exception_ce,
0 TSRMLS_CC,
"Error opening database file (%s). Is this a valid "
"MaxMind DB file?",
db_file);
efree(mmdb);
return;
}
maxminddb_obj *mmdb_obj = Z_MAXMINDDB_P(ZEND_THIS);
mmdb_obj->mmdb = mmdb;
}
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(
arginfo_maxminddbreader_get, 0, 1, IS_MIXED, 1)
ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, get) {
int prefix_len = 0;
get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, return_value, &prefix_len);
}
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(
arginfo_maxminddbreader_getWithPrefixLen, 0, 1, IS_ARRAY, 1)
ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, getWithPrefixLen) {
zval record, z_prefix_len;
int prefix_len = 0;
if (get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, &record, &prefix_len) ==
FAILURE) {
return;
}
array_init(return_value);
add_next_index_zval(return_value, &record);
ZVAL_LONG(&z_prefix_len, prefix_len);
add_next_index_zval(return_value, &z_prefix_len);
}
static int
get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len) {
char *ip_address = NULL;
strsize_t name_len;
zval *this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"Os",
&this_zval,
maxminddb_ce,
&ip_address,
&name_len) == FAILURE) {
return FAILURE;
}
const maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(ZEND_THIS);
MMDB_s *mmdb = mmdb_obj->mmdb;
if (NULL == mmdb) {
zend_throw_exception_ex(spl_ce_BadMethodCallException,
0 TSRMLS_CC,
"Attempt to read from a closed MaxMind DB.");
return FAILURE;
}
struct addrinfo hints = {
.ai_family = AF_UNSPEC,
.ai_flags = AI_NUMERICHOST,
/* We set ai_socktype so that we only get one result back */
.ai_socktype = SOCK_STREAM};
struct addrinfo *addresses = NULL;
int gai_status = getaddrinfo(ip_address, NULL, &hints, &addresses);
if (gai_status) {
zend_throw_exception_ex(spl_ce_InvalidArgumentException,
0 TSRMLS_CC,
"The value \"%s\" is not a valid IP address.",
ip_address);
return FAILURE;
}
if (!addresses || !addresses->ai_addr) {
zend_throw_exception_ex(
spl_ce_InvalidArgumentException,
0 TSRMLS_CC,
"getaddrinfo was successful but failed to set the addrinfo");
return FAILURE;
}
int sa_family = addresses->ai_addr->sa_family;
int mmdb_error = MMDB_SUCCESS;
MMDB_lookup_result_s result =
MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, &mmdb_error);
freeaddrinfo(addresses);
if (MMDB_SUCCESS != mmdb_error) {
zend_class_entry *ex;
if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
ex = spl_ce_InvalidArgumentException;
} else {
ex = maxminddb_exception_ce;
}
zend_throw_exception_ex(ex,
0 TSRMLS_CC,
"Error looking up %s. %s",
ip_address,
MMDB_strerror(mmdb_error));
return FAILURE;
}
*prefix_len = result.netmask;
if (sa_family == AF_INET && mmdb->metadata.ip_version == 6) {
/* We return the prefix length given the IPv4 address. If there is
no IPv4 subtree, we return a prefix length of 0. */
*prefix_len = *prefix_len >= 96 ? *prefix_len - 96 : 0;
}
if (!result.found_entry) {
ZVAL_NULL(record);
return SUCCESS;
}
MMDB_entry_data_list_s *entry_data_list = NULL;
int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
if (MMDB_SUCCESS != status) {
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Error while looking up data for %s. %s",
ip_address,
MMDB_strerror(status));
MMDB_free_entry_data_list(entry_data_list);
return FAILURE;
} else if (NULL == entry_data_list) {
zend_throw_exception_ex(
maxminddb_exception_ce,
0 TSRMLS_CC,
"Error while looking up data for %s. Your database may "
"be corrupt or you have found a bug in libmaxminddb.",
ip_address);
return FAILURE;
}
const MMDB_entry_data_list_s *rv =
handle_entry_data_list(entry_data_list, record TSRMLS_CC);
if (rv == NULL) {
/* We should have already thrown the exception in handle_entry_data_list
*/
return FAILURE;
}
MMDB_free_entry_data_list(entry_data_list);
return SUCCESS;
}
ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_void, 0, 0, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, metadata) {
zval *this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"O",
&this_zval,
maxminddb_ce) == FAILURE) {
return;
}
const maxminddb_obj *const mmdb_obj =
(maxminddb_obj *)Z_MAXMINDDB_P(this_zval);
if (NULL == mmdb_obj->mmdb) {
zend_throw_exception_ex(spl_ce_BadMethodCallException,
0 TSRMLS_CC,
"Attempt to read from a closed MaxMind DB.");
return;
}
object_init_ex(return_value, metadata_ce);
MMDB_entry_data_list_s *entry_data_list;
int status =
MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
if (status != MMDB_SUCCESS) {
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Error while decoding metadata. %s",
MMDB_strerror(status));
return;
}
zval metadata_array;
const MMDB_entry_data_list_s *rv =
handle_entry_data_list(entry_data_list, &metadata_array TSRMLS_CC);
if (rv == NULL) {
return;
}
MMDB_free_entry_data_list(entry_data_list);
zend_call_method_with_1_params(PROP_OBJ(return_value),
metadata_ce,
&metadata_ce->constructor,
ZEND_CONSTRUCTOR_FUNC_NAME,
NULL,
&metadata_array);
zval_ptr_dtor(&metadata_array);
}
PHP_METHOD(MaxMind_Db_Reader, close) {
zval *this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"O",
&this_zval,
maxminddb_ce) == FAILURE) {
return;
}
maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(this_zval);
if (NULL == mmdb_obj->mmdb) {
zend_throw_exception_ex(spl_ce_BadMethodCallException,
0 TSRMLS_CC,
"Attempt to close a closed MaxMind DB.");
return;
}
MMDB_close(mmdb_obj->mmdb);
efree(mmdb_obj->mmdb);
mmdb_obj->mmdb = NULL;
}
static const MMDB_entry_data_list_s *
handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
switch (entry_data_list->entry_data.type) {
case MMDB_DATA_TYPE_MAP:
return handle_map(entry_data_list, z_value TSRMLS_CC);
case MMDB_DATA_TYPE_ARRAY:
return handle_array(entry_data_list, z_value TSRMLS_CC);
case MMDB_DATA_TYPE_UTF8_STRING:
ZVAL_STRINGL(z_value,
entry_data_list->entry_data.utf8_string,
entry_data_list->entry_data.data_size);
break;
case MMDB_DATA_TYPE_BYTES:
ZVAL_STRINGL(z_value,
(char const *)entry_data_list->entry_data.bytes,
entry_data_list->entry_data.data_size);
break;
case MMDB_DATA_TYPE_DOUBLE:
ZVAL_DOUBLE(z_value, entry_data_list->entry_data.double_value);
break;
case MMDB_DATA_TYPE_FLOAT:
ZVAL_DOUBLE(z_value, entry_data_list->entry_data.float_value);
break;
case MMDB_DATA_TYPE_UINT16:
ZVAL_LONG(z_value, entry_data_list->entry_data.uint16);
break;
case MMDB_DATA_TYPE_UINT32:
handle_uint32(entry_data_list, z_value TSRMLS_CC);
break;
case MMDB_DATA_TYPE_BOOLEAN:
ZVAL_BOOL(z_value, entry_data_list->entry_data.boolean);
break;
case MMDB_DATA_TYPE_UINT64:
handle_uint64(entry_data_list, z_value TSRMLS_CC);
break;
case MMDB_DATA_TYPE_UINT128:
handle_uint128(entry_data_list, z_value TSRMLS_CC);
break;
case MMDB_DATA_TYPE_INT32:
ZVAL_LONG(z_value, entry_data_list->entry_data.int32);
break;
default:
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Invalid data type arguments: %d",
entry_data_list->entry_data.type);
return NULL;
}
return entry_data_list;
}
static const MMDB_entry_data_list_s *
handle_map(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
array_init(z_value);
const uint32_t map_size = entry_data_list->entry_data.data_size;
uint32_t i;
for (i = 0; i < map_size && entry_data_list; i++) {
entry_data_list = entry_data_list->next;
char *key = estrndup(entry_data_list->entry_data.utf8_string,
entry_data_list->entry_data.data_size);
if (NULL == key) {
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Invalid data type arguments");
return NULL;
}
entry_data_list = entry_data_list->next;
zval new_value;
entry_data_list =
handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC);
if (entry_data_list != NULL) {
add_assoc_zval(z_value, key, &new_value);
}
efree(key);
}
return entry_data_list;
}
static const MMDB_entry_data_list_s *
handle_array(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
const uint32_t size = entry_data_list->entry_data.data_size;
array_init(z_value);
uint32_t i;
for (i = 0; i < size && entry_data_list; i++) {
entry_data_list = entry_data_list->next;
zval new_value;
entry_data_list =
handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC);
if (entry_data_list != NULL) {
add_next_index_zval(z_value, &new_value);
}
}
return entry_data_list;
}
static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
uint64_t high = 0;
uint64_t low = 0;
#if MMDB_UINT128_IS_BYTE_ARRAY
int i;
for (i = 0; i < 8; i++) {
high = (high << 8) | entry_data_list->entry_data.uint128[i];
}
for (i = 8; i < 16; i++) {
low = (low << 8) | entry_data_list->entry_data.uint128[i];
}
#else
high = entry_data_list->entry_data.uint128 >> 64;
low = (uint64_t)entry_data_list->entry_data.uint128;
#endif
char *num_str;
spprintf(&num_str, 0, "0x%016" PRIX64 "%016" PRIX64, high, low);
CHECK_ALLOCATED(num_str);
ZVAL_STRING(z_value, num_str);
efree(num_str);
}
static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
uint32_t val = entry_data_list->entry_data.uint32;
#if LONG_MAX >= UINT32_MAX
ZVAL_LONG(z_value, val);
return;
#else
if (val <= LONG_MAX) {
ZVAL_LONG(z_value, val);
return;
}
char *int_str;
spprintf(&int_str, 0, "%" PRIu32, val);
CHECK_ALLOCATED(int_str);
ZVAL_STRING(z_value, int_str);
efree(int_str);
#endif
}
static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
uint64_t val = entry_data_list->entry_data.uint64;
#if LONG_MAX >= UINT64_MAX
ZVAL_LONG(z_value, val);
return;
#else
if (val <= LONG_MAX) {
ZVAL_LONG(z_value, val);
return;
}
char *int_str;
spprintf(&int_str, 0, "%" PRIu64, val);
CHECK_ALLOCATED(int_str);
ZVAL_STRING(z_value, int_str);
efree(int_str);
#endif
}
static void maxminddb_free_storage(free_obj_t *object TSRMLS_DC) {
maxminddb_obj *obj =
php_maxminddb_fetch_object((zend_object *)object TSRMLS_CC);
if (obj->mmdb != NULL) {
MMDB_close(obj->mmdb);
efree(obj->mmdb);
}
zend_object_std_dtor(&obj->std TSRMLS_CC);
}
static zend_object *maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) {
maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj));
zend_object_std_init(&obj->std, type TSRMLS_CC);
object_properties_init(&(obj->std), type);
obj->std.handlers = &maxminddb_obj_handlers;
return &obj->std;
}
/* clang-format off */
static zend_function_entry maxminddb_methods[] = {
PHP_ME(MaxMind_Db_Reader, __construct, arginfo_maxminddbreader_construct,
ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
PHP_ME(MaxMind_Db_Reader, close, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC)
PHP_ME(MaxMind_Db_Reader, get, arginfo_maxminddbreader_get, ZEND_ACC_PUBLIC)
PHP_ME(MaxMind_Db_Reader, getWithPrefixLen, arginfo_maxminddbreader_getWithPrefixLen, ZEND_ACC_PUBLIC)
PHP_ME(MaxMind_Db_Reader, metadata, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC)
{ NULL, NULL, NULL }
};
/* clang-format on */
ZEND_BEGIN_ARG_INFO_EX(arginfo_metadata_construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, metadata, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader_Metadata, __construct) {
zval *object = NULL;
zval *metadata_array = NULL;
zend_long node_count = 0;
zend_long record_size = 0;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"Oa",
&object,
metadata_ce,
&metadata_array) == FAILURE) {
return;
}
zval *tmp = NULL;
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"binary_format_major_version",
sizeof("binary_format_major_version") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"binaryFormatMajorVersion",
sizeof("binaryFormatMajorVersion") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"binary_format_minor_version",
sizeof("binary_format_minor_version") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"binaryFormatMinorVersion",
sizeof("binaryFormatMinorVersion") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"build_epoch",
sizeof("build_epoch") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"buildEpoch",
sizeof("buildEpoch") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"database_type",
sizeof("database_type") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"databaseType",
sizeof("databaseType") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"description",
sizeof("description") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"description",
sizeof("description") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"ip_version",
sizeof("ip_version") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"ipVersion",
sizeof("ipVersion") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(
HASH_OF(metadata_array), "languages", sizeof("languages") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"languages",
sizeof("languages") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"record_size",
sizeof("record_size") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"recordSize",
sizeof("recordSize") - 1,
tmp);
if (Z_TYPE_P(tmp) == IS_LONG) {
record_size = Z_LVAL_P(tmp);
}
}
if (record_size != 0) {
zend_update_property_long(metadata_ce,
PROP_OBJ(object),
"nodeByteSize",
sizeof("nodeByteSize") - 1,
record_size / 4);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"node_count",
sizeof("node_count") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"nodeCount",
sizeof("nodeCount") - 1,
tmp);
if (Z_TYPE_P(tmp) == IS_LONG) {
node_count = Z_LVAL_P(tmp);
}
}
if (record_size != 0) {
zend_update_property_long(metadata_ce,
PROP_OBJ(object),
"searchTreeSize",
sizeof("searchTreeSize") - 1,
record_size * node_count / 4);
}
}
// clang-format off
static zend_function_entry metadata_methods[] = {
PHP_ME(MaxMind_Db_Reader_Metadata, __construct, arginfo_metadata_construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
{NULL, NULL, NULL}
};
// clang-format on
PHP_MINIT_FUNCTION(maxminddb) {
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_EX_NS, NULL);
maxminddb_exception_ce =
zend_register_internal_class_ex(&ce, zend_ce_exception);
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_NS, maxminddb_methods);
maxminddb_ce = zend_register_internal_class(&ce TSRMLS_CC);
maxminddb_ce->create_object = maxminddb_create_handler;
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_METADATA_NS, metadata_methods);
metadata_ce = zend_register_internal_class(&ce TSRMLS_CC);
zend_declare_property_null(metadata_ce,
"binaryFormatMajorVersion",
sizeof("binaryFormatMajorVersion") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"binaryFormatMinorVersion",
sizeof("binaryFormatMinorVersion") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "buildEpoch", sizeof("buildEpoch") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"databaseType",
sizeof("databaseType") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "description", sizeof("description") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "ipVersion", sizeof("ipVersion") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "languages", sizeof("languages") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"nodeByteSize",
sizeof("nodeByteSize") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "nodeCount", sizeof("nodeCount") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "recordSize", sizeof("recordSize") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"searchTreeSize",
sizeof("searchTreeSize") - 1,
ZEND_ACC_PUBLIC);
memcpy(&maxminddb_obj_handlers,
zend_get_std_object_handlers(),
sizeof(zend_object_handlers));
maxminddb_obj_handlers.clone_obj = NULL;
maxminddb_obj_handlers.offset = XtOffsetOf(maxminddb_obj, std);
maxminddb_obj_handlers.free_obj = maxminddb_free_storage;
zend_declare_class_constant_string(maxminddb_ce,
"MMDB_LIB_VERSION",
sizeof("MMDB_LIB_VERSION") - 1,
MMDB_lib_version() TSRMLS_CC);
return SUCCESS;
}
static PHP_MINFO_FUNCTION(maxminddb) {
php_info_print_table_start();
php_info_print_table_row(2, "MaxMind DB Reader", "enabled");
php_info_print_table_row(
2, "maxminddb extension version", PHP_MAXMINDDB_VERSION);
php_info_print_table_row(
2, "libmaxminddb library version", MMDB_lib_version());
php_info_print_table_end();
}
zend_module_entry maxminddb_module_entry = {STANDARD_MODULE_HEADER,
PHP_MAXMINDDB_EXTNAME,
NULL,
PHP_MINIT(maxminddb),
NULL,
NULL,
NULL,
PHP_MINFO(maxminddb),
PHP_MAXMINDDB_VERSION,
STANDARD_MODULE_PROPERTIES};
#ifdef COMPILE_DL_MAXMINDDB
ZEND_GET_MODULE(maxminddb)
#endif
-24
View File
@@ -1,24 +0,0 @@
/* MaxMind, Inc., licenses this file to you under the Apache License, Version
* 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
#include <zend_interfaces.h>
#ifndef PHP_MAXMINDDB_H
#define PHP_MAXMINDDB_H 1
#define PHP_MAXMINDDB_VERSION "1.13.1"
#define PHP_MAXMINDDB_EXTNAME "maxminddb"
extern zend_module_entry maxminddb_module_entry;
#define phpext_maxminddb_ptr &maxminddb_module_entry
#endif
@@ -1,12 +0,0 @@
--TEST--
Check for maxminddb presence
--SKIPIF--
<?php if (!extension_loaded('maxminddb')) {
echo 'skip';
} ?>
--FILE--
<?php
echo 'maxminddb extension is available';
?>
--EXPECT--
maxminddb extension is available
@@ -1,13 +0,0 @@
--TEST--
Check that Reader class is not final
--SKIPIF--
<?php if (!extension_loaded('maxminddb')) {
echo 'skip';
} ?>
--FILE--
<?php
$reflectionClass = new \ReflectionClass('MaxMind\Db\Reader');
var_dump($reflectionClass->isFinal());
?>
--EXPECT--
bool(false)
@@ -1,12 +0,0 @@
--TEST--
openbase_dir is followed
--INI--
open_basedir=/--dne--
--FILE--
<?php
use MaxMind\Db\Reader;
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb');
?>
--EXPECTREGEX--
.*open_basedir restriction in effect.*
-61
View File
@@ -1,61 +0,0 @@
<?xml version="1.0"?>
<package version="2.0" xmlns="http://pear.php.net/dtd/package-2.0"
xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
<name>maxminddb</name>
<channel>pecl.php.net</channel>
<summary>Reader for the MaxMind DB file format</summary>
<description>This is the PHP extension for reading MaxMind DB files. MaxMind DB is a binary file format that stores data indexed by IP address subnets (IPv4 or IPv6).</description>
<lead>
<name>Greg Oschwald</name>
<user>oschwald</user>
<email>goschwald@maxmind.com</email>
<active>yes</active>
</lead>
<date>2025-11-21</date>
<version>
<release>1.13.1</release>
<api>1.13.1</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="https://github.com/maxmind/MaxMind-DB-Reader-php/blob/main/LICENSE">Apache License 2.0</license>
<notes>* First PIE release. No other changes.</notes>
<contents>
<dir name="/">
<file role="doc" name="LICENSE"/>
<file role="doc" name="CHANGELOG.md"/>
<file role="doc" name="README.md"/>
<dir name="ext">
<file role="src" name="config.m4"/>
<file role="src" name="config.w32"/>
<file role="src" name="maxminddb.c"/>
<file role="src" name="php_maxminddb.h"/>
<dir name="tests">
<file role="test" name="001-load.phpt"/>
<file role="test" name="002-final.phpt"/>
<file role="test" name="003-open-basedir.phpt"/>
</dir>
</dir>
</dir>
</contents>
<dependencies>
<required>
<php>
<min>7.2.0</min>
</php>
<pearinstaller>
<min>1.10.0</min>
</pearinstaller>
</required>
</dependencies>
<providesextension>maxminddb</providesextension>
<extsrcrelease />
</package>
@@ -1,404 +0,0 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db;
use MaxMind\Db\Reader\Decoder;
use MaxMind\Db\Reader\InvalidDatabaseException;
use MaxMind\Db\Reader\Metadata;
use MaxMind\Db\Reader\Util;
/**
* Instances of this class provide a reader for the MaxMind DB format. IP
* addresses can be looked up using the get method.
*/
class Reader
{
/**
* @var int
*/
private static $DATA_SECTION_SEPARATOR_SIZE = 16;
/**
* @var string
*/
private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
/**
* @var int<0, max>
*/
private static $METADATA_START_MARKER_LENGTH = 14;
/**
* @var int
*/
private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB
/**
* @var Decoder
*/
private $decoder;
/**
* @var resource
*/
private $fileHandle;
/**
* @var int
*/
private $fileSize;
/**
* @var int
*/
private $ipV4Start;
/**
* @var Metadata
*/
private $metadata;
/**
* Constructs a Reader for the MaxMind DB format. The file passed to it must
* be a valid MaxMind DB file such as a GeoIp2 database file.
*
* @param string $database the MaxMind DB file to use
*
* @throws \InvalidArgumentException for invalid database path or unknown arguments
* @throws InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*/
public function __construct(string $database)
{
if (\func_num_args() !== 1) {
throw new \ArgumentCountError(
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
if (is_dir($database)) {
// This matches the error that the C extension throws.
throw new InvalidDatabaseException(
"Error opening database file ($database). Is this a valid MaxMind DB file?"
);
}
$fileHandle = @fopen($database, 'rb');
if ($fileHandle === false) {
throw new \InvalidArgumentException(
"The file \"$database\" does not exist or is not readable."
);
}
$this->fileHandle = $fileHandle;
$fstat = fstat($fileHandle);
if ($fstat === false) {
throw new \UnexpectedValueException(
"Error determining the size of \"$database\"."
);
}
$this->fileSize = $fstat['size'];
$start = $this->findMetadataStart($database);
$metadataDecoder = new Decoder($this->fileHandle, $start);
[$metadataArray] = $metadataDecoder->decode($start);
$this->metadata = new Metadata($metadataArray);
$this->decoder = new Decoder(
$this->fileHandle,
$this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
);
$this->ipV4Start = $this->ipV4StartNode();
}
/**
* Retrieves the record for the IP address.
*
* @param string $ipAddress the IP address to look up
*
* @throws \BadMethodCallException if this method is called on a closed database
* @throws \InvalidArgumentException if something other than a single IP address is passed to the method
* @throws InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*
* @return mixed the record for the IP address
*/
public function get(string $ipAddress)
{
if (\func_num_args() !== 1) {
throw new \ArgumentCountError(
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
[$record] = $this->getWithPrefixLen($ipAddress);
return $record;
}
/**
* Retrieves the record for the IP address and its associated network prefix length.
*
* @param string $ipAddress the IP address to look up
*
* @throws \BadMethodCallException if this method is called on a closed database
* @throws \InvalidArgumentException if something other than a single IP address is passed to the method
* @throws InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*
* @return array{0:mixed, 1:int} an array where the first element is the record and the
* second the network prefix length for the record
*/
public function getWithPrefixLen(string $ipAddress): array
{
if (\func_num_args() !== 1) {
throw new \ArgumentCountError(
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
if (!\is_resource($this->fileHandle)) {
throw new \BadMethodCallException(
'Attempt to read from a closed MaxMind DB.'
);
}
[$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
if ($pointer === 0) {
return [null, $prefixLen];
}
return [$this->resolveDataPointer($pointer), $prefixLen];
}
/**
* @return array{0:int, 1:int}
*/
private function findAddressInTree(string $ipAddress): array
{
$packedAddr = @inet_pton($ipAddress);
if ($packedAddr === false) {
throw new \InvalidArgumentException(
"The value \"$ipAddress\" is not a valid IP address."
);
}
$rawAddress = unpack('C*', $packedAddr);
if ($rawAddress === false) {
throw new InvalidDatabaseException(
'Could not unpack the unsigned char of the packed in_addr representation.'
);
}
$bitCount = \count($rawAddress) * 8;
// The first node of the tree is always node 0, at the beginning of the
// value
$node = 0;
$metadata = $this->metadata;
// Check if we are looking up an IPv4 address in an IPv6 tree. If this
// is the case, we can skip over the first 96 nodes.
if ($metadata->ipVersion === 6) {
if ($bitCount === 32) {
$node = $this->ipV4Start;
}
} elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
throw new \InvalidArgumentException(
"Error looking up $ipAddress. You attempted to look up an"
. ' IPv6 address in an IPv4-only database.'
);
}
$nodeCount = $metadata->nodeCount;
for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
$tempBit = 0xFF & $rawAddress[($i >> 3) + 1];
$bit = 1 & ($tempBit >> 7 - ($i % 8));
$node = $this->readNode($node, $bit);
}
if ($node === $nodeCount) {
// Record is empty
return [0, $i];
}
if ($node > $nodeCount) {
// Record is a data pointer
return [$node, $i];
}
throw new InvalidDatabaseException(
'Invalid or corrupt database. Maximum search depth reached without finding a leaf node'
);
}
private function ipV4StartNode(): int
{
// If we have an IPv4 database, the start node is the first node
if ($this->metadata->ipVersion === 4) {
return 0;
}
$node = 0;
for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
$node = $this->readNode($node, 0);
}
return $node;
}
private function readNode(int $nodeNumber, int $index): int
{
$baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
switch ($this->metadata->recordSize) {
case 24:
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
$rc = unpack('N', "\x00" . $bytes);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack the unsigned long of the node.'
);
}
[, $node] = $rc;
return $node;
case 28:
$bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
if ($index === 0) {
$middle = (0xF0 & \ord($bytes[3])) >> 4;
} else {
$middle = 0x0F & \ord($bytes[0]);
}
$rc = unpack('N', \chr($middle) . substr($bytes, $index, 3));
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack the unsigned long of the node.'
);
}
[, $node] = $rc;
return $node;
case 32:
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
$rc = unpack('N', $bytes);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack the unsigned long of the node.'
);
}
[, $node] = $rc;
return $node;
default:
throw new InvalidDatabaseException(
'Unknown record size: '
. $this->metadata->recordSize
);
}
}
/**
* @return mixed
*/
private function resolveDataPointer(int $pointer)
{
$resolved = $pointer - $this->metadata->nodeCount
+ $this->metadata->searchTreeSize;
if ($resolved >= $this->fileSize) {
throw new InvalidDatabaseException(
"The MaxMind DB file's search tree is corrupt"
);
}
[$data] = $this->decoder->decode($resolved);
return $data;
}
/*
* This is an extremely naive but reasonably readable implementation. There
* are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
* an issue, but I suspect it won't be.
*/
private function findMetadataStart(string $filename): int
{
$handle = $this->fileHandle;
$fileSize = $this->fileSize;
$marker = self::$METADATA_START_MARKER;
$markerLength = self::$METADATA_START_MARKER_LENGTH;
$minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize);
for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
if (fseek($handle, $offset) !== 0) {
break;
}
$value = fread($handle, $markerLength);
if ($value === $marker) {
return $offset + $markerLength;
}
}
throw new InvalidDatabaseException(
"Error opening database file ($filename). "
. 'Is this a valid MaxMind DB file?'
);
}
/**
* @throws \InvalidArgumentException if arguments are passed to the method
* @throws \BadMethodCallException if the database has been closed
*
* @return Metadata object for the database
*/
public function metadata(): Metadata
{
if (\func_num_args()) {
throw new \ArgumentCountError(
\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
);
}
// Not technically required, but this makes it consistent with
// C extension and it allows us to change our implementation later.
if (!\is_resource($this->fileHandle)) {
throw new \BadMethodCallException(
'Attempt to read from a closed MaxMind DB.'
);
}
return clone $this->metadata;
}
/**
* Closes the MaxMind DB and returns resources to the system.
*
* @throws \Exception
* if an I/O error occurs
*/
public function close(): void
{
if (\func_num_args()) {
throw new \ArgumentCountError(
\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
);
}
if (!\is_resource($this->fileHandle)) {
throw new \BadMethodCallException(
'Attempt to close a closed MaxMind DB.'
);
}
fclose($this->fileHandle);
}
}
@@ -1,452 +0,0 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
// @codingStandardsIgnoreLine
class Decoder
{
/**
* @var resource
*/
private $fileStream;
/**
* @var int
*/
private $pointerBase;
/**
* This is only used for unit testing.
*
* @var bool
*/
private $pointerTestHack;
/**
* @var bool
*/
private $switchByteOrder;
private const _EXTENDED = 0;
private const _POINTER = 1;
private const _UTF8_STRING = 2;
private const _DOUBLE = 3;
private const _BYTES = 4;
private const _UINT16 = 5;
private const _UINT32 = 6;
private const _MAP = 7;
private const _INT32 = 8;
private const _UINT64 = 9;
private const _UINT128 = 10;
private const _ARRAY = 11;
// 12 is the container type
// 13 is the end marker type
private const _BOOLEAN = 14;
private const _FLOAT = 15;
/**
* @param resource $fileStream
*/
public function __construct(
$fileStream,
int $pointerBase = 0,
bool $pointerTestHack = false
) {
$this->fileStream = $fileStream;
$this->pointerBase = $pointerBase;
$this->pointerTestHack = $pointerTestHack;
$this->switchByteOrder = $this->isPlatformLittleEndian();
}
/**
* @return array<mixed>
*/
public function decode(int $offset): array
{
$ctrlByte = \ord(Util::read($this->fileStream, $offset, 1));
++$offset;
$type = $ctrlByte >> 5;
// Pointers are a special case, we don't read the next $size bytes, we
// use the size to determine the length of the pointer and then follow
// it.
if ($type === self::_POINTER) {
[$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);
// for unit testing
if ($this->pointerTestHack) {
return [$pointer];
}
[$result] = $this->decode($pointer);
return [$result, $offset];
}
if ($type === self::_EXTENDED) {
$nextByte = \ord(Util::read($this->fileStream, $offset, 1));
$type = $nextByte + 7;
if ($type < 8) {
throw new InvalidDatabaseException(
'Something went horribly wrong in the decoder. An extended type '
. 'resolved to a type number < 8 ('
. $type
. ')'
);
}
++$offset;
}
[$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);
return $this->decodeByType($type, $offset, $size);
}
/**
* @param int<0, max> $size
*
* @return array{0:mixed, 1:int}
*/
private function decodeByType(int $type, int $offset, int $size): array
{
switch ($type) {
case self::_MAP:
return $this->decodeMap($size, $offset);
case self::_ARRAY:
return $this->decodeArray($size, $offset);
case self::_BOOLEAN:
return [$this->decodeBoolean($size), $offset];
}
$newOffset = $offset + $size;
$bytes = Util::read($this->fileStream, $offset, $size);
switch ($type) {
case self::_BYTES:
case self::_UTF8_STRING:
return [$bytes, $newOffset];
case self::_DOUBLE:
$this->verifySize(8, $size);
return [$this->decodeDouble($bytes), $newOffset];
case self::_FLOAT:
$this->verifySize(4, $size);
return [$this->decodeFloat($bytes), $newOffset];
case self::_INT32:
return [$this->decodeInt32($bytes, $size), $newOffset];
case self::_UINT16:
case self::_UINT32:
case self::_UINT64:
case self::_UINT128:
return [$this->decodeUint($bytes, $size), $newOffset];
default:
throw new InvalidDatabaseException(
'Unknown or unexpected type: ' . $type
);
}
}
private function verifySize(int $expected, int $actual): void
{
if ($expected !== $actual) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
);
}
}
/**
* @return array{0:array<mixed>, 1:int}
*/
private function decodeArray(int $size, int $offset): array
{
$array = [];
for ($i = 0; $i < $size; ++$i) {
[$value, $offset] = $this->decode($offset);
$array[] = $value;
}
return [$array, $offset];
}
private function decodeBoolean(int $size): bool
{
return $size !== 0;
}
private function decodeDouble(string $bytes): float
{
// This assumes IEEE 754 doubles, but most (all?) modern platforms
// use them.
$rc = unpack('E', $bytes);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack a double value from the given bytes.'
);
}
[, $double] = $rc;
return $double;
}
private function decodeFloat(string $bytes): float
{
// This assumes IEEE 754 floats, but most (all?) modern platforms
// use them.
$rc = unpack('G', $bytes);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack a float value from the given bytes.'
);
}
[, $float] = $rc;
return $float;
}
private function decodeInt32(string $bytes, int $size): int
{
switch ($size) {
case 0:
return 0;
case 1:
case 2:
case 3:
$bytes = str_pad($bytes, 4, "\x00", \STR_PAD_LEFT);
break;
case 4:
break;
default:
throw new InvalidDatabaseException(
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
);
}
$rc = unpack('l', $this->maybeSwitchByteOrder($bytes));
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack a 32bit integer value from the given bytes.'
);
}
[, $int] = $rc;
return $int;
}
/**
* @return array{0:array<string, mixed>, 1:int}
*/
private function decodeMap(int $size, int $offset): array
{
$map = [];
for ($i = 0; $i < $size; ++$i) {
[$key, $offset] = $this->decode($offset);
[$value, $offset] = $this->decode($offset);
$map[$key] = $value;
}
return [$map, $offset];
}
/**
* @return array{0:int, 1:int}
*/
private function decodePointer(int $ctrlByte, int $offset): array
{
$pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
$buffer = Util::read($this->fileStream, $offset, $pointerSize);
$offset += $pointerSize;
switch ($pointerSize) {
case 1:
$packed = \chr($ctrlByte & 0x7) . $buffer;
$rc = unpack('n', $packed);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack an unsigned short value from the given bytes (pointerSize is 1).'
);
}
[, $pointer] = $rc;
$pointer += $this->pointerBase;
break;
case 2:
$packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
$rc = unpack('N', $packed);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack an unsigned long value from the given bytes (pointerSize is 2).'
);
}
[, $pointer] = $rc;
$pointer += $this->pointerBase + 2048;
break;
case 3:
$packed = \chr($ctrlByte & 0x7) . $buffer;
// It is safe to use 'N' here, even on 32 bit machines as the
// first bit is 0.
$rc = unpack('N', $packed);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack an unsigned long value from the given bytes (pointerSize is 3).'
);
}
[, $pointer] = $rc;
$pointer += $this->pointerBase + 526336;
break;
case 4:
// We cannot use unpack here as we might overflow on 32 bit
// machines
$pointerOffset = $this->decodeUint($buffer, $pointerSize);
$pointerBase = $this->pointerBase;
if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) {
$pointer = $pointerOffset + $pointerBase;
} else {
throw new \RuntimeException(
'The database offset is too large to be represented on your platform.'
);
}
break;
default:
throw new InvalidDatabaseException(
'Unexpected pointer size ' . $pointerSize
);
}
return [$pointer, $offset];
}
// @phpstan-ignore-next-line
private function decodeUint(string $bytes, int $byteLength)
{
if ($byteLength === 0) {
return 0;
}
// PHP integers are signed. PHP_INT_SIZE - 1 is the number of
// complete bytes that can be converted to an integer. However,
// we can convert another byte if the leading bit is zero.
$useRealInts = $byteLength <= \PHP_INT_SIZE - 1
|| ($byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0);
if ($useRealInts) {
$integer = 0;
for ($i = 0; $i < $byteLength; ++$i) {
$part = \ord($bytes[$i]);
$integer = ($integer << 8) + $part;
}
return $integer;
}
// We only use gmp or bcmath if the final value is too big
$integerAsString = '0';
for ($i = 0; $i < $byteLength; ++$i) {
$part = \ord($bytes[$i]);
if (\extension_loaded('gmp')) {
$integerAsString = gmp_strval(gmp_add(gmp_mul($integerAsString, '256'), $part));
} elseif (\extension_loaded('bcmath')) {
$integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part);
} else {
throw new \RuntimeException(
'The gmp or bcmath extension must be installed to read this database.'
);
}
}
return $integerAsString;
}
/**
* @return array{0:int, 1:int}
*/
private function sizeFromCtrlByte(int $ctrlByte, int $offset): array
{
$size = $ctrlByte & 0x1F;
if ($size < 29) {
return [$size, $offset];
}
$bytesToRead = $size - 28;
$bytes = Util::read($this->fileStream, $offset, $bytesToRead);
if ($size === 29) {
$size = 29 + \ord($bytes);
} elseif ($size === 30) {
$rc = unpack('n', $bytes);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack an unsigned short value from the given bytes.'
);
}
[, $adjust] = $rc;
$size = 285 + $adjust;
} else {
$rc = unpack('N', "\x00" . $bytes);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack an unsigned long value from the given bytes.'
);
}
[, $adjust] = $rc;
$size = $adjust + 65821;
}
return [$size, $offset + $bytesToRead];
}
private function maybeSwitchByteOrder(string $bytes): string
{
return $this->switchByteOrder ? strrev($bytes) : $bytes;
}
private function isPlatformLittleEndian(): bool
{
$testint = 0x00FF;
$packed = pack('S', $testint);
$rc = unpack('v', $packed);
if ($rc === false) {
throw new InvalidDatabaseException(
'Could not unpack an unsigned short value from the given bytes.'
);
}
return $testint === current($rc);
}
}
@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
/**
* This class should be thrown when unexpected data is found in the database.
*/
// phpcs:disable
class InvalidDatabaseException extends \Exception {}
@@ -1,123 +0,0 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
/**
* This class provides the metadata for the MaxMind DB file.
*/
class Metadata
{
/**
* This is an unsigned 16-bit integer indicating the major version number
* for the database's binary format.
*
* @var int
*/
public $binaryFormatMajorVersion;
/**
* This is an unsigned 16-bit integer indicating the minor version number
* for the database's binary format.
*
* @var int
*/
public $binaryFormatMinorVersion;
/**
* This is an unsigned 64-bit integer that contains the database build
* timestamp as a Unix epoch value.
*
* @var int
*/
public $buildEpoch;
/**
* This is a string that indicates the structure of each data record
* associated with an IP address. The actual definition of these
* structures is left up to the database creator.
*
* @var string
*/
public $databaseType;
/**
* This key will always point to a map (associative array). The keys of
* that map will be language codes, and the values will be a description
* in that language as a UTF-8 string. May be undefined for some
* databases.
*
* @var array<string, string>
*/
public $description;
/**
* This is an unsigned 16-bit integer which is always 4 or 6. It indicates
* whether the database contains IPv4 or IPv6 address data.
*
* @var int
*/
public $ipVersion;
/**
* An array of strings, each of which is a language code. A given record
* may contain data items that have been localized to some or all of
* these languages. This may be undefined.
*
* @var array<string>
*/
public $languages;
/**
* @var int
*/
public $nodeByteSize;
/**
* This is an unsigned 32-bit integer indicating the number of nodes in
* the search tree.
*
* @var int
*/
public $nodeCount;
/**
* This is an unsigned 16-bit integer. It indicates the number of bits in a
* record in the search tree. Note that each node consists of two records.
*
* @var int
*/
public $recordSize;
/**
* @var int
*/
public $searchTreeSize;
/**
* @param array<string, mixed> $metadata
*/
public function __construct(array $metadata)
{
if (\func_num_args() !== 1) {
throw new \ArgumentCountError(
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
$this->binaryFormatMajorVersion
= $metadata['binary_format_major_version'];
$this->binaryFormatMinorVersion
= $metadata['binary_format_minor_version'];
$this->buildEpoch = $metadata['build_epoch'];
$this->databaseType = $metadata['database_type'];
$this->languages = $metadata['languages'];
$this->description = $metadata['description'];
$this->ipVersion = $metadata['ip_version'];
$this->nodeCount = $metadata['node_count'];
$this->recordSize = $metadata['record_size'];
$this->nodeByteSize = $this->recordSize / 4;
$this->searchTreeSize = $this->nodeCount * $this->nodeByteSize;
}
}
@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
class Util
{
/**
* @param resource $stream
* @param int<0, max> $numberOfBytes
*/
public static function read($stream, int $offset, int $numberOfBytes): string
{
if ($numberOfBytes === 0) {
return '';
}
if (fseek($stream, $offset) === 0) {
$value = fread($stream, $numberOfBytes);
// We check that the number of bytes read is equal to the number
// asked for. We use ftell as getting the length of $value is
// much slower.
if ($value !== false && ftell($stream) - $offset === $numberOfBytes) {
return $value;
}
}
throw new InvalidDatabaseException(
'The MaxMind DB file contains bad data'
);
}
}