Files
KantSites/src/Ops/Environment.php
T
kantarci 495b26896e
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Fassung 1.7.0: verlässliche Installation, übersichtliche Verwaltung und bedienbare Bewegung
2026-09-14 10:49:54 +02:00

113 lines
4.6 KiB
PHP

<?php
namespace App\Ops;
use Symfony\Component\Dotenv\Dotenv;
/** One installation file, with an explicit, recoverable transition from older releases. */
final class Environment
{
public static function prepare(string $root): void
{
if (is_file($root.'/.env') && !is_file($root.'/.env.local')) {
return;
}
self::locked($root, static function () use ($root): void {
$file = $root.'/.env';
if (!is_file($file)) {
$template = is_file($root.'/.env.example') ? (string) file_get_contents($root.'/.env.example') : '';
self::replace($file, $template);
}
$legacy = $root.'/.env.local';
if (is_file($legacy)) {
// Preserve the previously effective configuration, not a release's default DB.
$base = (string) file_get_contents($file);
$old = (string) file_get_contents($legacy);
$values = (new Dotenv())->parse($base."\n".$old, $legacy);
if (preg_match('/^DATABASE_URL=/m', $old)) {
$values['KANT_DATABASE_CONFIGURED'] = '1';
}
$directory = $root.'/var/backups/environment-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(4));
if (!@mkdir($directory, 0o700, true)) {
throw new \RuntimeException('Die bisherige Konfiguration konnte nicht gesichert werden. Bitte die Schreibrechte auf var/backups prüfen.');
}
self::replace($directory.'/.env', $base);
self::replace($directory.'/.env.local', $old);
self::replace($file, self::encode($values));
if (!@unlink($legacy)) {
throw new \RuntimeException('Die Einstellungen sind in .env gesichert, aber .env.local konnte nicht entfernt werden. Bitte deren Schreibrechte prüfen.');
}
}
});
}
public static function write(string $root, array $values): void
{
self::locked($root, static function () use ($root, $values): void {
$file = $root.'/.env';
$existing = is_file($file) ? (new Dotenv())->parse((string) file_get_contents($file), $file) : [];
self::replace($file, self::encode(array_replace($existing, $values)));
});
}
/** A file lock serializes concurrent installer requests and secret generation. */
private static function locked(string $root, callable $operation): void
{
$lock = @fopen($root.'/.env.lock', 'c');
if (false === $lock || !flock($lock, LOCK_EX)) {
if (is_resource($lock)) {
fclose($lock);
}
throw new \RuntimeException('.env ist nicht beschreibbar. Bitte die Schreibrechte im Projektstamm prüfen.');
}
try {
$operation();
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}
public static function ensureSecret(string $root): string
{
$secret = '';
self::locked($root, static function () use ($root, &$secret): void {
$file = $root.'/.env';
$values = (new Dotenv())->parse((string) file_get_contents($file), $file);
$secret = $values['APP_SECRET'] ?? '';
if ('' === $secret) {
$secret = bin2hex(random_bytes(32));
self::replace($file, self::encode([...$values, 'APP_SECRET' => $secret]));
}
});
return $secret;
}
private static function encode(array $values): string
{
$lines = ['# Konfiguration dieser Installation. Updates erhalten diese Datei.'];
foreach ($values as $key => $value) {
if (1 !== preg_match('/^[A-Z_][A-Z0-9_]*$/D', $key)) {
throw new \InvalidArgumentException('Ungültiger Name einer Konfigurationseinstellung.');
}
$lines[] = $key.'="'.str_replace(['\\', '"', '$', "\n", "\r"], ['\\\\', '\\"', '\\$', '\\n', '\\r'], (string) $value).'"';
}
return implode("\n", $lines)."\n";
}
private static function replace(string $file, string $content): void
{
$temporary = $file.'.'.bin2hex(random_bytes(6)).'.tmp';
if (false === @file_put_contents($temporary, $content)) {
throw new \RuntimeException('.env ließ sich nicht schreiben. Bitte die Schreibrechte im Projektstamm prüfen.');
}
@chmod($temporary, 0o600);
if (!@rename($temporary, $file)) {
@unlink($temporary);
throw new \RuntimeException('.env ließ sich nicht ersetzen. Die bisherige Konfiguration bleibt erhalten.');
}
}
}