113 lines
4.6 KiB
PHP
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.');
|
|
}
|
|
}
|
|
}
|