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>
98 lines
3.3 KiB
PHP
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');
|
|
}
|
|
}
|