128 lines
4.9 KiB
PHP
128 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/** Atomic incremental installer for the confirmed-feedback update. */
|
|
function v4ConfirmedWrite(string $path, string $content, int $mode = 0644): void
|
|
{
|
|
if (is_link($path)) {
|
|
throw new RuntimeException('Symlink wird nicht ersetzt.');
|
|
}
|
|
$temp = tempnam(dirname($path), '.v4-confirmed-');
|
|
if ($temp === false) {
|
|
throw new RuntimeException('Temporaere Datei nicht verfuegbar.');
|
|
}
|
|
try {
|
|
if (file_put_contents($temp, $content, LOCK_EX) !== strlen($content)
|
|
|| !chmod($temp, $mode)
|
|
|| !rename($temp, $path)) {
|
|
throw new RuntimeException('Atomarer Dateitausch fehlgeschlagen.');
|
|
}
|
|
} finally {
|
|
if (is_file($temp)) {
|
|
unlink($temp);
|
|
}
|
|
}
|
|
}
|
|
|
|
function v4ConfirmedFiles(string $stage, string $target): array
|
|
{
|
|
$allowed = [
|
|
'libs/NetzfahrplanV4Rueckmeldung.php',
|
|
'libs/NetzfahrplanV4Geraeteabruf.php',
|
|
'libs/BatterieNetzfahrplanV4RueckmeldungTrait.php',
|
|
];
|
|
$manifest = json_decode((string) file_get_contents($stage . '/MANIFEST.json'), true, 32, JSON_THROW_ON_ERROR);
|
|
if (($manifest['scope'] ?? null) !== 'confirmed_feedback_trial_disabled'
|
|
|| count($manifest['files'] ?? []) !== count($allowed)
|
|
|| array_diff(array_keys($manifest['files']), $allowed)
|
|
|| array_diff($allowed, array_keys($manifest['files']))) {
|
|
throw new RuntimeException('Unerwarteter Dateiumfang.');
|
|
}
|
|
if (realpath($target) !== $target || is_link($target)) {
|
|
throw new RuntimeException('Modulpfad unerwartet.');
|
|
}
|
|
foreach ($manifest['dependencies'] as $name => $hash) {
|
|
if (!preg_match('~^(libs/[A-Za-z0-9]+\\.php|Batterie/module\\.php|Manager/module\\.php)$~D', $name)
|
|
|| is_link($target . '/' . $name)
|
|
|| hash_file('sha256', $target . '/' . $name) !== $hash) {
|
|
throw new RuntimeException('Abhaengigkeit wurde parallel geaendert: ' . $name);
|
|
}
|
|
}
|
|
|
|
$before = [];
|
|
$source = [];
|
|
$already = true;
|
|
foreach ($allowed as $name) {
|
|
$path = $target . '/' . $name;
|
|
$candidate = $stage . '/source/' . $name;
|
|
if (is_link($path) || is_link($candidate) || realpath(dirname($path)) !== $target . '/' . dirname($name)) {
|
|
throw new RuntimeException('Unerwarteter Dateipfad.');
|
|
}
|
|
$source[$name] = (string) file_get_contents($candidate);
|
|
if (hash('sha256', $source[$name]) !== $manifest['files'][$name]['after']) {
|
|
throw new RuntimeException('Paketpruefsumme geaendert: ' . $name);
|
|
}
|
|
token_get_all($source[$name], TOKEN_PARSE);
|
|
$before[$name] = is_file($path) ? file_get_contents($path) : null;
|
|
$hash = $before[$name] === null ? null : hash('sha256', $before[$name]);
|
|
if (!in_array($hash, [$manifest['files'][$name]['before'], $manifest['files'][$name]['after']], true)) {
|
|
throw new RuntimeException('Paralleler Modulstand; nichts ueberschrieben: ' . $name);
|
|
}
|
|
if ($hash !== $manifest['files'][$name]['after']) {
|
|
$already = false;
|
|
}
|
|
}
|
|
if ($already) {
|
|
return ['status' => 'already_installed', 'backup' => null, 'filesChanged' => []];
|
|
}
|
|
|
|
$backup = $stage . '/backups/' . gmdate('Ymd\\THis\\Z') . '-' . bin2hex(random_bytes(4));
|
|
if (!mkdir($backup, 0700, true)) {
|
|
throw new RuntimeException('Sicherung fehlgeschlagen.');
|
|
}
|
|
foreach ($allowed as $name) {
|
|
if ($before[$name] !== null) {
|
|
$destination = $backup . '/' . $name;
|
|
if (!is_dir(dirname($destination))) {
|
|
mkdir(dirname($destination), 0700, true);
|
|
}
|
|
v4ConfirmedWrite($destination, $before[$name], 0600);
|
|
}
|
|
}
|
|
v4ConfirmedWrite(
|
|
$backup . '/MANIFEST.json',
|
|
json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . "\n",
|
|
0600
|
|
);
|
|
|
|
$written = [];
|
|
try {
|
|
foreach ($allowed as $name) {
|
|
if ((is_file($target . '/' . $name) ? file_get_contents($target . '/' . $name) : null) !== $before[$name]) {
|
|
throw new RuntimeException('Parallele Aenderung waehrend Installation.');
|
|
}
|
|
if ($before[$name] === $source[$name]) {
|
|
continue;
|
|
}
|
|
v4ConfirmedWrite($target . '/' . $name, $source[$name]);
|
|
$written[] = $name;
|
|
}
|
|
} catch (Throwable $error) {
|
|
foreach (array_reverse($written) as $name) {
|
|
$path = $target . '/' . $name;
|
|
if (!is_link($path) && is_file($path)
|
|
&& hash_file('sha256', $path) === $manifest['files'][$name]['after']) {
|
|
if ($before[$name] === null) {
|
|
unlink($path);
|
|
} else {
|
|
v4ConfirmedWrite($path, $before[$name]);
|
|
}
|
|
}
|
|
}
|
|
throw $error;
|
|
}
|
|
|
|
return ['status' => 'installed', 'backup' => $backup, 'filesChanged' => $written];
|
|
}
|