Force HTTPS from the admin, with a probe before and after
System > HTTPS and cms:https switch the redirect lines in public/.htaccess only when the site answers over HTTPS, and take them back on a loop. HSTS is a separate second switch. The redirect block moves before the front controller rules, where it never reached pages, and .well-known is no longer denied, so the origin probe works on Apache. The Git updater keeps switched-on lines like the archive update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
daad5a1740
commit
8b212d5cc9
15 files changed
+614
-24
No files matched your search
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Ops\HttpsRedirect;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'cms:https', description: 'Shows or switches the HTTPS redirect and HSTS in public/.htaccess, with a probe before and after (ADR 120).')]
|
||||
final class HttpsCommand extends Command
|
||||
{
|
||||
public function __construct(private HttpsRedirect $https, private string $siteOrigin)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('aktion', InputArgument::OPTIONAL, 'an, aus, hsts-an oder hsts-aus; ohne Angabe der Stand.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$action = (string) $input->getArgument('aktion');
|
||||
if ('' === $action) {
|
||||
$state = $this->https->state();
|
||||
$io->writeln(sprintf('Umleitung auf HTTPS: %s · HSTS: %s%s', $state['redirect'] ? 'an' : 'aus', $state['hsts'] ? 'an' : 'aus', $state['found'] ? '' : ' · Zeilen fehlen in public/.htaccess'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$result = match ($action) {
|
||||
'an' => $this->https->enableRedirect($this->siteOrigin),
|
||||
'aus' => $this->https->disableRedirect(),
|
||||
'hsts-an' => $this->https->switchHsts(true, $this->siteOrigin),
|
||||
'hsts-aus' => $this->https->switchHsts(false, $this->siteOrigin),
|
||||
default => ['ok' => false, 'message' => 'Erlaubt sind an, aus, hsts-an und hsts-aus.'],
|
||||
};
|
||||
$result['ok'] ? $io->success($result['message']) : $io->error($result['message']);
|
||||
|
||||
return $result['ok'] ? Command::SUCCESS : Command::FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Domain\InvalidPage;
|
||||
use App\Ops\ArchiveUpdate;
|
||||
use App\Ops\Backup;
|
||||
use App\Ops\Environment;
|
||||
use App\Ops\HttpsRedirect;
|
||||
use App\Ops\Installation;
|
||||
use App\Ops\OriginCheck;
|
||||
use App\Ops\SitePackage;
|
||||
@@ -29,7 +30,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
*/
|
||||
final class SystemController extends AbstractController
|
||||
{
|
||||
public function __construct(private Updater $updater, private Backup $backup, private ArchiveUpdate $archive, private Installation $installation, private string $adminPath, private string $databaseUrl, private string $mediaDirectory, private string $projectDir, private string $siteOrigin, private OriginCheck $originCheck, private HttpKernelInterface $kernel, private PageRepository $pages, private SiteSettings $settings, private SitePackage $packages)
|
||||
public function __construct(private Updater $updater, private Backup $backup, private ArchiveUpdate $archive, private Installation $installation, private string $adminPath, private string $databaseUrl, private string $mediaDirectory, private string $projectDir, private string $siteOrigin, private OriginCheck $originCheck, private HttpKernelInterface $kernel, private PageRepository $pages, private SiteSettings $settings, private SitePackage $packages, private HttpsRedirect $https)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -67,6 +68,9 @@ final class SystemController extends AbstractController
|
||||
// bekommt hier den Hinweis, dass die Website woanders eingetragen ist.
|
||||
'origin' => $this->originFacts($request),
|
||||
'originRejected' => $request->getSession()->remove('origin_rejected'),
|
||||
// HTTPS erzwingen und HSTS (ADR 120): der Stand in public/.htaccess.
|
||||
'https' => $this->https->state(),
|
||||
'secure' => $request->isSecure(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -494,6 +498,28 @@ final class SystemController extends AbstractController
|
||||
), 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPS erzwingen oder HSTS schalten (ADR 120). Die Umleitung wird nur eingeschaltet, wenn
|
||||
* HTTPS vorher und nachher hier ankommt; sonst bleibt public/.htaccess, wie sie war.
|
||||
*/
|
||||
#[Route('/%kant.admin_path%/system/https', name: 'admin_system_https', methods: ['POST'])]
|
||||
public function https(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) {
|
||||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||||
}
|
||||
$result = match ($request->request->getString('action')) {
|
||||
'redirect-an' => $this->https->enableRedirect($this->siteOrigin),
|
||||
'redirect-aus' => $this->https->disableRedirect(),
|
||||
'hsts-an' => $this->https->switchHsts(true, $this->siteOrigin),
|
||||
'hsts-aus' => $this->https->switchHsts(false, $this->siteOrigin),
|
||||
default => ['ok' => false, 'message' => 'Unbekannte Aktion.'],
|
||||
};
|
||||
|
||||
return $this->back($result['message'], $result['ok'] ? 'success' : 'error');
|
||||
}
|
||||
|
||||
/** Die Gegenstelle zur Prüfung oben: öffentlich, und sie verrät nichts als diese Kennung. */
|
||||
#[Route(OriginCheck::PATH, name: 'site_origin_probe', methods: ['GET'], priority: 10)]
|
||||
public function originProbe(): Response
|
||||
|
||||
@@ -566,15 +566,7 @@ final class ArchiveUpdate
|
||||
*/
|
||||
private function mergeHtaccess(string $neu, string $alt): void
|
||||
{
|
||||
$vorher = (string) file_get_contents($alt);
|
||||
$inhalt = (string) file_get_contents($neu);
|
||||
foreach (['RewriteCond %{HTTPS} !=on', 'RewriteCond %{HTTP:X-Forwarded-Proto} !=https', 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]', 'Header always set Strict-Transport-Security'] as $zeile) {
|
||||
$aktiv = 1 === preg_match('/^\s*'.preg_quote($zeile, '/').'/m', $vorher);
|
||||
if ($aktiv) {
|
||||
$inhalt = (string) preg_replace('/^(\s*)#\s*('.preg_quote($zeile, '/').')/m', '$1$2', $inhalt);
|
||||
}
|
||||
}
|
||||
file_put_contents($alt, $inhalt);
|
||||
file_put_contents($alt, HttpsRedirect::keepEnabled((string) file_get_contents($neu), (string) file_get_contents($alt)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ops;
|
||||
|
||||
/**
|
||||
* Schaltet die Umleitung auf HTTPS und HSTS in public/.htaccess ein und aus (ADR 120).
|
||||
*
|
||||
* Die Zeilen stehen ausgeliefert auskommentiert in der Datei: Eine Installation ohne Zertifikat
|
||||
* liefe mit ihnen in eine Endlosschleife, und die Anwendung selbst erzwingt kein HTTPS, weil
|
||||
* PHP hinter einem Proxy oft nur http sieht. Bisher hieß das: per FTP in die .htaccess. Dieser
|
||||
* Schalter macht dasselbe aus der Verwaltung, aber nur mit Probe davor und danach:
|
||||
*
|
||||
* 1. Antwortet diese Installation unter https://<Adresse> mit gültigem Zertifikat? Sonst nichts.
|
||||
* 2. Zeilen einschalten.
|
||||
* 3. Kommt https://<Adresse> weiterhin hier an? Sonst ist es eine Schleife (der Server meldet
|
||||
* HTTPS nicht, kein Proxy-Kopf) - die Datei wird sofort zurückgeschrieben.
|
||||
* 4. Leitet http://<Adresse> mit 301 auf https um? Wenn nicht, liest der Server die Regel nicht
|
||||
* (kein mod_rewrite, AllowOverride) - die Zeilen bleiben, schaden nicht, und die Meldung sagt es.
|
||||
*
|
||||
* HSTS ist ein eigener Schritt: Ein Browser merkt sich den Kopf ein Jahr lang, ein Rückweg auf
|
||||
* http ist dann für jeden Besucher zu, der die Seite einmal gesehen hat.
|
||||
*/
|
||||
class HttpsRedirect
|
||||
{
|
||||
public const REDIRECT_LINES = [
|
||||
'RewriteCond %{HTTPS} !=on',
|
||||
'RewriteCond %{HTTP:X-Forwarded-Proto} !=https',
|
||||
'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]',
|
||||
];
|
||||
public const HSTS_LINE = 'Header always set Strict-Transport-Security';
|
||||
|
||||
/** @var \Closure(string): array{status: int, location: string, error: string} */
|
||||
private \Closure $httpProbe;
|
||||
|
||||
/** @param \Closure(string): array{status: int, location: string, error: string}|null $httpProbe */
|
||||
public function __construct(private string $projectDir, private OriginCheck $originCheck, ?\Closure $httpProbe = null)
|
||||
{
|
||||
$this->httpProbe = $httpProbe ?? self::probeHttp(...);
|
||||
}
|
||||
|
||||
public function file(): string
|
||||
{
|
||||
return $this->projectDir.'/public/.htaccess';
|
||||
}
|
||||
|
||||
/** @return array{redirect: bool, hsts: bool, found: bool, writable: bool} */
|
||||
public function state(): array
|
||||
{
|
||||
$content = is_file($this->file()) ? (string) file_get_contents($this->file()) : '';
|
||||
|
||||
return [
|
||||
'redirect' => self::active($content, self::REDIRECT_LINES),
|
||||
'hsts' => self::active($content, [self::HSTS_LINE]),
|
||||
'found' => self::present($content, self::REDIRECT_LINES) && self::present($content, [self::HSTS_LINE]),
|
||||
'writable' => is_file($this->file()) && is_writable($this->file()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schaltet die Umleitung ein. Gibt Erfolg und eine Meldung in einem Satz oder zwei zurück;
|
||||
* bei Misserfolg ist die Datei so, wie sie vorher war.
|
||||
*
|
||||
* @return array{ok: bool, message: string}
|
||||
*/
|
||||
public function enableRedirect(string $origin): array
|
||||
{
|
||||
$origin = rtrim(trim($origin), '/');
|
||||
if (!preg_match('#^https://[^/?\#]+$#', $origin)) {
|
||||
return ['ok' => false, 'message' => 'Erst die Adresse der Website mit https:// eintragen – die Umleitung braucht ein Ziel.'];
|
||||
}
|
||||
if (null !== $problem = $this->originCheck->problem($origin)) {
|
||||
return ['ok' => false, 'message' => $problem.' Ohne funktionierendes HTTPS bleibt die Umleitung aus.'];
|
||||
}
|
||||
$before = $this->read();
|
||||
if (null === $before) {
|
||||
return ['ok' => false, 'message' => 'public/.htaccess fehlt oder ist nicht lesbar.'];
|
||||
}
|
||||
if (!self::present($before, self::REDIRECT_LINES)) {
|
||||
return ['ok' => false, 'message' => 'Die Zeilen für HTTPS stehen nicht in public/.htaccess (geändert oder sehr alte Fassung). Das nächste Update bringt sie zurück.'];
|
||||
}
|
||||
if (self::active($before, self::REDIRECT_LINES)) {
|
||||
return ['ok' => true, 'message' => 'Die Umleitung auf HTTPS ist schon eingeschaltet.'];
|
||||
}
|
||||
if (!$this->write(self::switchLines($before, self::REDIRECT_LINES, true))) {
|
||||
return ['ok' => false, 'message' => 'PHP darf public/.htaccess nicht schreiben. Die drei Zeilen unter „HTTPS erzwingen“ dort von Hand einschalten (das # davor entfernen).'];
|
||||
}
|
||||
if (null !== $problem = $this->originCheck->problem($origin)) {
|
||||
$this->write($before);
|
||||
|
||||
return ['ok' => false, 'message' => 'Mit eingeschalteter Umleitung kam '.$origin.' nicht mehr an: '.rtrim($problem, '.').'. Vermutlich meldet der Server HTTPS nicht an die .htaccess (Proxy ohne X-Forwarded-Proto). Die Datei ist zurückgesetzt, nichts geändert.'];
|
||||
}
|
||||
$http = 'http://'.substr($origin, 8).OriginCheck::PATH;
|
||||
$answer = ($this->httpProbe)($http);
|
||||
$target = $origin.OriginCheck::PATH;
|
||||
if (in_array($answer['status'], [301, 308], true) && $answer['location'] === $target) {
|
||||
return ['ok' => true, 'message' => 'HTTPS wird jetzt erzwungen: http:// leitet mit 301 auf '.$origin.' um. HSTS erst einschalten, wenn das ein paar Tage gut läuft.'];
|
||||
}
|
||||
if (0 === $answer['status']) {
|
||||
return ['ok' => true, 'message' => 'Die Umleitung ist eingeschaltet, und HTTPS antwortet weiter. Ob http:// umleitet, ließ sich vom Server aus nicht prüfen ('.$answer['error'].') – einmal im Browser http:// aufrufen.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => sprintf('Die Zeilen sind eingeschaltet, aber http:// antwortet mit %d%s statt mit 301 auf HTTPS. Der Server liest die Umleitung offenbar nicht (mod_rewrite, AllowOverride) – beim Hoster die HTTPS-Weiterleitung einschalten.', $answer['status'], '' !== $answer['location'] ? ' nach '.$answer['location'] : '')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schaltet die Umleitung aus - und HSTS mit, denn HSTS ohne Umleitung verspricht etwas,
|
||||
* das der Server nicht mehr einhält.
|
||||
*
|
||||
* @return array{ok: bool, message: string}
|
||||
*/
|
||||
public function disableRedirect(): array
|
||||
{
|
||||
$before = $this->read();
|
||||
if (null === $before) {
|
||||
return ['ok' => false, 'message' => 'public/.htaccess fehlt oder ist nicht lesbar.'];
|
||||
}
|
||||
$hsts = self::active($before, [self::HSTS_LINE]);
|
||||
$after = self::switchLines(self::switchLines($before, self::REDIRECT_LINES, false), [self::HSTS_LINE], false);
|
||||
if ($after !== $before && !$this->write($after)) {
|
||||
return ['ok' => false, 'message' => 'PHP darf public/.htaccess nicht schreiben.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => 'Die Umleitung auf HTTPS ist ausgeschaltet.'.($hsts ? ' HSTS ebenfalls – Browser, die den Kopf schon kennen, bleiben aber bis zu einem Jahr bei HTTPS.' : '')];
|
||||
}
|
||||
|
||||
/** @return array{ok: bool, message: string} */
|
||||
public function switchHsts(bool $on, string $origin): array
|
||||
{
|
||||
$before = $this->read();
|
||||
if (null === $before || !self::present($before, [self::HSTS_LINE])) {
|
||||
return ['ok' => false, 'message' => 'Die HSTS-Zeile steht nicht in public/.htaccess.'];
|
||||
}
|
||||
if ($on) {
|
||||
if (!self::active($before, self::REDIRECT_LINES)) {
|
||||
return ['ok' => false, 'message' => 'HSTS erst, wenn die Umleitung auf HTTPS eingeschaltet ist.'];
|
||||
}
|
||||
if (null !== $problem = $this->originCheck->problem(rtrim(trim($origin), '/'))) {
|
||||
return ['ok' => false, 'message' => $problem.' HSTS bleibt aus.'];
|
||||
}
|
||||
}
|
||||
if (!$this->write(self::switchLines($before, [self::HSTS_LINE], $on))) {
|
||||
return ['ok' => false, 'message' => 'PHP darf public/.htaccess nicht schreiben.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => $on
|
||||
? 'HSTS ist eingeschaltet: Browser rufen diese Domain ein Jahr lang nur noch über HTTPS auf, Subdomains eingeschlossen.'
|
||||
: 'HSTS ist ausgeschaltet. Browser, die den Kopf schon kennen, bleiben bis zu einem Jahr bei HTTPS.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Die neue .htaccess eines Updates, mit den Zeilen eingeschaltet, die in der alten
|
||||
* eingeschaltet waren. Archiv- und Git-Update rufen beide hierher.
|
||||
*/
|
||||
public static function keepEnabled(string $new, string $old): string
|
||||
{
|
||||
if (self::active($old, self::REDIRECT_LINES)) {
|
||||
$new = self::switchLines($new, self::REDIRECT_LINES, true);
|
||||
}
|
||||
if (self::active($old, [self::HSTS_LINE])) {
|
||||
$new = self::switchLines($new, [self::HSTS_LINE], true);
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/** @param list<string> $lines */
|
||||
public static function active(string $content, array $lines): bool
|
||||
{
|
||||
foreach ($lines as $line) {
|
||||
if (1 !== preg_match('/^[ \t]*'.preg_quote($line, '/').'/m', $content)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param list<string> $lines */
|
||||
private static function present(string $content, array $lines): bool
|
||||
{
|
||||
foreach ($lines as $line) {
|
||||
if (1 !== preg_match('/^[ \t]*(#[ \t]*)?'.preg_quote($line, '/').'/m', $content)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param list<string> $lines */
|
||||
private static function switchLines(string $content, array $lines, bool $on): string
|
||||
{
|
||||
foreach ($lines as $line) {
|
||||
$quoted = preg_quote($line, '/');
|
||||
$content = $on
|
||||
? (string) preg_replace('/^([ \t]*)#[ \t]*('.$quoted.')/m', '$1$2', $content)
|
||||
: (string) preg_replace('/^([ \t]*)('.$quoted.')/m', '$1#$2', $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function read(): ?string
|
||||
{
|
||||
$content = is_file($this->file()) ? @file_get_contents($this->file()) : false;
|
||||
|
||||
return false === $content ? null : $content;
|
||||
}
|
||||
|
||||
private function write(string $content): bool
|
||||
{
|
||||
return is_writable($this->file()) && false !== @file_put_contents($this->file(), $content, LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Anfrage über http, ohne Weiterleitungen zu folgen: gefragt ist nur, wohin sie zeigt.
|
||||
*
|
||||
* @return array{status: int, location: string, error: string}
|
||||
*/
|
||||
private static function probeHttp(string $url): array
|
||||
{
|
||||
if (function_exists('curl_init')) {
|
||||
$curl = curl_init($url);
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_USERAGENT => 'KantSites-HTTPS-Check',
|
||||
]);
|
||||
$raw = curl_exec($curl);
|
||||
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$error = curl_error($curl);
|
||||
curl_close($curl);
|
||||
$location = is_string($raw) && preg_match('/^Location:[ \t]*(\S+)/mi', $raw, $match) ? $match[1] : '';
|
||||
|
||||
return ['status' => $status, 'location' => $location, 'error' => '' !== $error ? $error : 'keine Antwort'];
|
||||
}
|
||||
$context = stream_context_create(['http' => ['follow_location' => 0, 'ignore_errors' => true, 'timeout' => 10, 'user_agent' => 'KantSites-HTTPS-Check']]);
|
||||
$http_response_header = [];
|
||||
$body = @file_get_contents($url, false, $context);
|
||||
$headers = $http_response_header;
|
||||
if (false === $body && [] === $headers) {
|
||||
return ['status' => 0, 'location' => '', 'error' => 'keine Verbindung über http'];
|
||||
}
|
||||
$status = preg_match('#^HTTP/\S+\s+(\d{3})#', (string) ($headers[0] ?? ''), $match) ? (int) $match[1] : 0;
|
||||
$location = '';
|
||||
foreach ($headers as $header) {
|
||||
if (preg_match('/^Location:[ \t]*(\S+)/i', $header, $match)) {
|
||||
$location = $match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return ['status' => $status, 'location' => $location, 'error' => 'keine Antwort'];
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -96,7 +96,7 @@ final class Updater
|
||||
'git' => $git,
|
||||
// Der Weg ueber Git steht nur einer Arbeitskopie offen.
|
||||
'worktree' => $arbeitskopie,
|
||||
'clean' => null === $status ? null : ('' === $status['output'] && $status['ok']),
|
||||
'clean' => null === $status ? null : ([] === self::dirty($status['output']) && $status['ok']),
|
||||
'log' => $this->log(),
|
||||
];
|
||||
}
|
||||
@@ -138,10 +138,10 @@ final class Updater
|
||||
}
|
||||
|
||||
$status = $this->git(['status', '--porcelain']);
|
||||
$dirty = array_values(array_filter(explode("\n", $status['output'])));
|
||||
$dirty = self::dirty($status['output']);
|
||||
$add($checks, 'Keine lokalen Änderungen', [] === $dirty, [] === $dirty
|
||||
? 'Die Arbeitskopie ist unverändert.'
|
||||
: sprintf('%d geänderte Datei(en), zum Beispiel %s. Ein Checkout würde sie verwerfen. Erst sichern oder zurücknehmen.', count($dirty), trim(explode("\n", $status['output'])[0])));
|
||||
: sprintf('%d geänderte Datei(en), zum Beispiel %s. Ein Checkout würde sie verwerfen. Erst sichern oder zurücknehmen.', count($dirty), trim($dirty[0])));
|
||||
|
||||
$remote = $this->git(['remote', 'get-url', 'origin']);
|
||||
$add($checks, 'Herkunft', $remote['ok'], $remote['ok']
|
||||
@@ -299,8 +299,13 @@ final class Updater
|
||||
throw new \RuntimeException('Die alte Git-Konfiguration konnte nicht für den Updatewechsel vorbereitet werden.');
|
||||
}
|
||||
}
|
||||
$htaccess = $this->root.'/public/.htaccess';
|
||||
$htaccessBefore = is_file($htaccess) ? (string) file_get_contents($htaccess) : null;
|
||||
$checkout = $this->git(['checkout', '--force', $target, '--']);
|
||||
$this->restoreEnvironment();
|
||||
if (null !== $htaccessBefore && is_file($htaccess)) {
|
||||
@file_put_contents($htaccess, HttpsRedirect::keepEnabled((string) file_get_contents($htaccess), $htaccessBefore), LOCK_EX);
|
||||
}
|
||||
if (!$step($steps, 'Code wechseln', $checkout['ok'], $checkout['ok'] ? 'Auf '.$target.' gewechselt.' : $checkout['output'])) {
|
||||
throw new \RuntimeException('checkout');
|
||||
}
|
||||
@@ -444,6 +449,18 @@ final class Updater
|
||||
}
|
||||
|
||||
/** Legacy tags tracked .env; preserve the installation even when checking out those tags. */
|
||||
/**
|
||||
* Die geänderten Dateien laut `git status --porcelain` - ohne public/.htaccess: Eine dort
|
||||
* eingeschaltete HTTPS-Umleitung nimmt apply() in die neue Fassung mit (ADR 120), sie ist
|
||||
* kein Grund, das Update zu verweigern.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function dirty(string $porcelain): array
|
||||
{
|
||||
return array_values(array_filter(explode("\n", $porcelain), static fn (string $line): bool => '' !== trim($line) && 1 !== preg_match('#^\s*M{1,2}\s+public/\.htaccess$#', rtrim($line))));
|
||||
}
|
||||
|
||||
private function restoreEnvironment(): void
|
||||
{
|
||||
foreach ($this->environmentBefore as $file => $content) {
|
||||
|
||||
Reference in new issue
Block a user