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>
71 lines
2.7 KiB
PHP
71 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Zentrale;
|
|
|
|
/**
|
|
* Die neueste veröffentlichte KantSites-Fassung - von derselben Releaseseite, die auch die
|
|
* Installationen fragen. Eine halbe Stunde zwischengespeichert; ist die Releaseseite nicht
|
|
* erreichbar, gilt der letzte bekannte Stand.
|
|
*/
|
|
final class Fassungen
|
|
{
|
|
private const GUELTIG = 1800;
|
|
|
|
public function __construct(private string $api, private string $cache)
|
|
{
|
|
}
|
|
|
|
/** @return array{version: string, name: string, published: string}|null */
|
|
public function neueste(bool $frisch = false): ?array
|
|
{
|
|
$gespeichert = json_decode((string) @file_get_contents($this->cache), true);
|
|
$gespeichert = is_array($gespeichert) && is_string($gespeichert['version'] ?? null) ? $gespeichert : null;
|
|
if (!$frisch && null !== $gespeichert && (int) ($gespeichert['geholt'] ?? 0) > time() - self::GUELTIG) {
|
|
return $gespeichert;
|
|
}
|
|
$neu = $this->holen();
|
|
if (null === $neu) {
|
|
return $gespeichert;
|
|
}
|
|
@file_put_contents($this->cache, json_encode([...$neu, 'geholt' => time()]));
|
|
|
|
return $neu;
|
|
}
|
|
|
|
/** @return array{version: string, name: string, published: string}|null */
|
|
private function holen(): ?array
|
|
{
|
|
if (!str_starts_with($this->api, 'https://')) {
|
|
return null;
|
|
}
|
|
$kontext = stream_context_create([
|
|
'http' => ['timeout' => 8, 'ignore_errors' => true, 'header' => "User-Agent: KantSites-Zentrale\r\nAccept: application/json\r\n"],
|
|
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
|
|
]);
|
|
if (function_exists('curl_init')) {
|
|
$curl = curl_init(rtrim($this->api, '/').'/releases/latest');
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
|
CURLOPT_CONNECTTIMEOUT => 5,
|
|
CURLOPT_TIMEOUT => 8,
|
|
CURLOPT_USERAGENT => 'KantSites-Zentrale',
|
|
]);
|
|
$antwort = curl_exec($curl);
|
|
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
|
curl_close($curl);
|
|
$antwort = 200 === $status ? $antwort : false;
|
|
} else {
|
|
$antwort = @file_get_contents(rtrim($this->api, '/').'/releases/latest', false, $kontext);
|
|
}
|
|
$daten = is_string($antwort) ? json_decode($antwort, true) : null;
|
|
if (!is_array($daten) || 1 !== preg_match('/^v(\d+\.\d+\.\d+)$/D', (string) ($daten['tag_name'] ?? ''), $treffer)) {
|
|
return null;
|
|
}
|
|
|
|
return ['version' => $treffer[1], 'name' => (string) ($daten['name'] ?? ''), 'published' => (string) ($daten['published_at'] ?? '')];
|
|
}
|
|
}
|