Redirect the other www spelling to SITE_ORIGIN, hold roles to the role help and fix the audit findings
SITE_ORIGIN alone decides whether the site is www or not: CanonicalHostSubscriber sends the other spelling there with 301, cms:site:origin writes the setting, the system screen shows it. The roles now match the role help (editor: drafts, publisher: live content, admin: design, settings, users, system). The installer no longer reopens on an unreachable database, the session cookie follows ADMIN_PATH, percent-encoded credentials no longer break the container, consent only accepts same-site decisions, JSON-LD is HTML-safe, own CSS stays in its block, plus the editor, updater, download and public-output findings from the full review (ADR 099, ADR 100). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
45040b2ca6
commit
7eb462ac7b
53 files changed
+1023
-122
No files matched your search
@@ -25,6 +25,66 @@ final class CommandRunner
|
||||
return !in_array('proc_open', $disabled, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Was ein Kindprozess von der Umgebung sehen darf: Pfade, ein Zuhause fuer Composer und Git,
|
||||
* aber kein APP_ENV, das einen Konsolenbefehl gegen die falsche Umgebung laufen liesse.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function environment(): array
|
||||
{
|
||||
$environment = ['PATH' => getenv('PATH') ?: '', 'SystemRoot' => getenv('SystemRoot') ?: '', 'TEMP' => sys_get_temp_dir(), 'TMP' => sys_get_temp_dir()];
|
||||
// Composer verlangt HOME (Linux) oder APPDATA (Windows) - ohne sie bricht `composer
|
||||
// install` ab, waehrend `composer --version` noch laeuft. Git braucht HOME fuer .gitconfig.
|
||||
foreach (['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'COMPOSER_HOME', 'PATHEXT', 'LANG', 'LC_ALL'] as $name) {
|
||||
$value = getenv($name);
|
||||
if (is_string($value) && '' !== $value) {
|
||||
$environment[$name] = $value;
|
||||
}
|
||||
}
|
||||
if (!isset($environment['HOME']) && !isset($environment['USERPROFILE'])) {
|
||||
// Unter php-fpm ist HOME oft leer; ein Ordner in var/ tut es fuer Composer und Git.
|
||||
$environment['HOME'] = sys_get_temp_dir();
|
||||
}
|
||||
if (!isset($environment['COMPOSER_HOME'])) {
|
||||
$environment['COMPOSER_HOME'] = $environment['HOME'] ?? sys_get_temp_dir();
|
||||
}
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die PHP-Kommandozeile, mit der Konsolenbefehle laufen.
|
||||
*
|
||||
* Aus einem Webprozess ist PHP_BINARY der Server selbst (php-fpm) oder leer (mod_php) - und
|
||||
* `php-fpm bin/console` startet keinen Befehl. Dann wird die Kommandozeile im Pfad gesucht.
|
||||
*/
|
||||
public function php(): string
|
||||
{
|
||||
if (isset($this->resolved['php'])) {
|
||||
return $this->resolved['php'][0];
|
||||
}
|
||||
$candidates = [];
|
||||
if ('cli' === \PHP_SAPI && '' !== \PHP_BINARY) {
|
||||
$candidates[] = \PHP_BINARY;
|
||||
}
|
||||
$suffix = '\\' === \DIRECTORY_SEPARATOR ? '.exe' : '';
|
||||
foreach ([\PHP_BINDIR.'/php'.$suffix, \PHP_BINDIR.'/php'.\PHP_MAJOR_VERSION.'.'.\PHP_MINOR_VERSION.$suffix, 'php'.$suffix, 'php'.\PHP_MAJOR_VERSION.'.'.\PHP_MINOR_VERSION.$suffix, \PHP_BINARY] as $candidate) {
|
||||
if ('' !== $candidate && !\in_array($candidate, $candidates, true)) {
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
foreach ($candidates as $candidate) {
|
||||
if ($this->run([$candidate, '-r', 'echo PHP_VERSION;'], sys_get_temp_dir(), 30)['ok']) {
|
||||
$this->resolved['php'] = [$candidate];
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates[0] ?? 'php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $command
|
||||
*
|
||||
@@ -43,7 +103,7 @@ final class CommandRunner
|
||||
$cwd,
|
||||
// A minimal, predictable environment: no inherited APP_ENV that would make a console
|
||||
// command run against the wrong environment.
|
||||
['PATH' => getenv('PATH') ?: '', 'SystemRoot' => getenv('SystemRoot') ?: '', 'TEMP' => sys_get_temp_dir()],
|
||||
$this->environment(),
|
||||
);
|
||||
if (!is_resource($process)) {
|
||||
return ['ok' => false, 'exit' => -1, 'output' => sprintf('%s konnte nicht gestartet werden.', $command[0])];
|
||||
@@ -109,7 +169,7 @@ final class CommandRunner
|
||||
}
|
||||
// A phar in the project is the way on hosting where nothing is installed globally.
|
||||
if (is_file($cwd.'/'.$executable.'.phar')) {
|
||||
$candidates[] = [PHP_BINARY, $cwd.'/'.$executable.'.phar'];
|
||||
$candidates[] = [$this->php(), $cwd.'/'.$executable.'.phar'];
|
||||
}
|
||||
foreach ($candidates as $candidate) {
|
||||
if ($this->run([...$candidate, '--version'], $cwd, 30)['ok']) {
|
||||
|
||||
+21
-6
@@ -63,6 +63,10 @@ class Download
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 3,
|
||||
// Auch eine Weiterleitung bleibt bei HTTPS: sonst pruefte die erste Zeile nur die
|
||||
// erste Adresse, und die Gegenstelle koennte auf http:// umlenken.
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
|
||||
@@ -96,12 +100,23 @@ class Download
|
||||
|
||||
return null;
|
||||
}
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => $this->timeout, 'follow_location' => 1, 'max_redirects' => 3, 'header' => "User-Agent: KantSites-Updater\r\n"],
|
||||
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
|
||||
]);
|
||||
$antwort = @file_get_contents($url, false, $context);
|
||||
if (is_string($antwort)) {
|
||||
// Der Dateizeiger folgt Weiterleitungen auch auf http://; deshalb hier von Hand, und
|
||||
// jede Station muss HTTPS sein.
|
||||
$antwort = false;
|
||||
for ($hop = 0; $hop <= 3 && str_starts_with($url, 'https://'); ++$hop) {
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => $this->timeout, 'follow_location' => 0, 'ignore_errors' => true, 'header' => "User-Agent: KantSites-Updater\r\n"],
|
||||
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
|
||||
]);
|
||||
$antwort = @file_get_contents($url, false, $context);
|
||||
$kopf = implode("\n", $http_response_header ?? []);
|
||||
if (!is_string($antwort) || 1 !== preg_match('/^HTTP\/\S+ 3\d\d/m', $kopf) || 1 !== preg_match('/^Location:\s*(\S+)/mi', $kopf, $ziel)) {
|
||||
break;
|
||||
}
|
||||
$url = $ziel[1];
|
||||
$antwort = false;
|
||||
}
|
||||
if (is_string($antwort) && 1 === preg_match('/^HTTP\/\S+ 200/m', implode("\n", $http_response_header ?? []))) {
|
||||
$this->grund = '';
|
||||
|
||||
return $antwort;
|
||||
|
||||
@@ -9,8 +9,10 @@ use App\Content\PartialRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use App\Domain\CmsUser;
|
||||
use App\Domain\InvalidPage;
|
||||
use App\EventSubscriber\InstallSubscriber;
|
||||
use App\Infrastructure\ConnectionFactory;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
|
||||
use Doctrine\Migrations\Configuration\Migration\PhpFile;
|
||||
use Doctrine\Migrations\DependencyFactory;
|
||||
@@ -57,11 +59,22 @@ final class Installation
|
||||
*/
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
// Die Marke schreibt die Website, sobald ein Konto existiert. Sie zaehlt auch dann, wenn
|
||||
// die Datenbank gerade nicht antwortet: ein gedrehtes Passwort oder eine Wartung beim
|
||||
// Hoster darf den Installer nicht wieder oeffnen - dort koennte sonst jeder eine eigene
|
||||
// Datenbank eintragen und sich zum Administrator machen.
|
||||
if (is_file($this->root.'/'.InstallSubscriber::MARKER)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return (bool) $this->db->fetchOne('SELECT id FROM cms_user LIMIT 1');
|
||||
} catch (\Throwable) {
|
||||
// No database, no table, no installation - all three mean the same here.
|
||||
} catch (TableNotFoundException) {
|
||||
// Keine Tabelle: die Datenbank ist da, aber leer - das ist der Installer-Fall.
|
||||
return false;
|
||||
} catch (\Throwable) {
|
||||
// Keine Verbindung. Vor dem Datenbankschritt ist das normal (es gibt noch keine
|
||||
// eingetragene Datenbank); danach ist es eine Stoerung, keine fehlende Installation.
|
||||
return $this->environmentWritten();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -29,7 +29,7 @@ final class Updater
|
||||
private string $databaseUrl,
|
||||
private CommandRunner $runner,
|
||||
private Connection $db,
|
||||
private string $php = PHP_BINARY,
|
||||
private ?string $php = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -288,7 +288,12 @@ final class Updater
|
||||
if (!$step($steps, 'Rauchtest', $health['ok'], $this->lastLine($health['output']))) {
|
||||
throw new \RuntimeException('health');
|
||||
}
|
||||
} catch (\RuntimeException) {
|
||||
} catch (\Throwable $error) {
|
||||
// Nicht nur die eigenen Abbrueche: jeder Fehler auf dem Weg nimmt den Rueckweg, statt
|
||||
// den Wartungszustand ohne ein Wort stehen zu lassen.
|
||||
if (!$error instanceof \RuntimeException) {
|
||||
$step($steps, 'Unerwarteter Fehler', false, $error->getMessage());
|
||||
}
|
||||
$this->rollback($steps, $from, $migrated ? $backup : null, $step);
|
||||
$this->write($steps, $target, false);
|
||||
|
||||
@@ -438,7 +443,7 @@ final class Updater
|
||||
/** @return array{ok: bool, exit: int, output: string} */
|
||||
private function console(array $arguments, int $timeout): array
|
||||
{
|
||||
return $this->runner->run([$this->php, ...$arguments], $this->root, $timeout);
|
||||
return $this->runner->run([$this->php ??= $this->runner->php(), ...$arguments], $this->root, $timeout);
|
||||
}
|
||||
|
||||
/** The SQLite file behind DATABASE_URL, or null for a server database. */
|
||||
|
||||
Reference in new issue
Block a user