Files
KantSites/zentrale/app/Datenbank.php
T
kantarciandClaude Opus 5.5 723680b7de Add the central site registry and remote updates
Every installation now reports address and version to sites.kant-it.de
every 15 minutes and picks up update orders there (ADR 123). Reports and
answers are HMAC-signed with a per-installation key sent on first
contact; an order can only install a published release with checksum.
The next request after the file swap runs the migrations, so a remote
update finishes without anyone in the admin.

The platform itself lives in zentrale/ (plain PHP + SQLite, not part of
the release archive): overview with outdated, silent and unassigned
installations, single and bulk updates, per-site detail and history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:43:13 +02:00

98 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Zentrale;
/**
* SQLite in var/zentrale.db. Das Schema legt sich beim ersten Aufruf selbst an; spätere
* Änderungen kommen als nummerierte Schritte in MIGRATIONEN dazu, nie als Änderung eines alten.
*/
final class Datenbank
{
private const MIGRATIONEN = [
1 => [
'CREATE TABLE konto (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, passwort TEXT NOT NULL, erstellt TEXT NOT NULL)',
'CREATE TABLE anmeldeversuch (ip TEXT NOT NULL, zeit TEXT NOT NULL)',
'CREATE INDEX anmeldeversuch_ip ON anmeldeversuch (ip, zeit)',
'CREATE TABLE installation (
id TEXT PRIMARY KEY,
schluessel TEXT NOT NULL,
adresse TEXT NOT NULL,
domain TEXT NOT NULL,
domain_vorher TEXT,
fassung TEXT NOT NULL,
php TEXT NOT NULL DEFAULT \'\',
datenbank TEXT NOT NULL DEFAULT \'\',
art TEXT NOT NULL DEFAULT \'archiv\',
wartung INTEGER NOT NULL DEFAULT 0,
update_phase TEXT,
update_meldung TEXT,
update_zeit TEXT,
notiz TEXT NOT NULL DEFAULT \'\',
ip TEXT NOT NULL DEFAULT \'\',
erste_meldung TEXT NOT NULL,
letzte_meldung TEXT NOT NULL
)',
'CREATE TABLE auftrag (
id INTEGER PRIMARY KEY,
installation TEXT NOT NULL REFERENCES installation (id) ON DELETE CASCADE,
aktion TEXT NOT NULL,
fassung TEXT NOT NULL,
erstellt TEXT NOT NULL,
abgeholt TEXT,
erledigt TEXT,
ok INTEGER,
meldung TEXT
)',
'CREATE INDEX auftrag_installation ON auftrag (installation, id)',
],
];
public static function oeffnen(string $datei): \PDO
{
$neu = !is_file($datei);
if ($neu && !is_dir(dirname($datei)) && !@mkdir(dirname($datei), 0o775, true)) {
throw new \RuntimeException(dirname($datei).' lässt sich nicht anlegen.');
}
$pdo = new \PDO('sqlite:'.$datei, null, null, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
]);
$pdo->exec('PRAGMA foreign_keys = ON');
$pdo->exec('PRAGMA busy_timeout = 5000');
if ($neu) {
$pdo->exec('PRAGMA journal_mode = WAL');
}
self::migrieren($pdo);
return $pdo;
}
private static function migrieren(\PDO $pdo): void
{
$stand = (int) $pdo->query('PRAGMA user_version')->fetchColumn();
foreach (self::MIGRATIONEN as $nummer => $anweisungen) {
if ($nummer <= $stand) {
continue;
}
$pdo->beginTransaction();
try {
foreach ($anweisungen as $sql) {
$pdo->exec($sql);
}
$pdo->exec('PRAGMA user_version = '.$nummer);
$pdo->commit();
} catch (\Throwable $fehler) {
$pdo->rollBack();
throw $fehler;
}
}
}
public static function jetzt(): string
{
return gmdate('Y-m-d\TH:i:s\Z');
}
}