From bbda7e546447359aecc991661d99bd0bd8a57878 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Fri, 26 Apr 2024 23:40:10 +0000 Subject: [PATCH] Wip2 --- composer.json | 2 + config/openssl-cert-request | 8 ++ config/php-fpm.conf | 4 +- lib/Commands/Compile.php | 85 ++++++++++++---- lib/Commands/Keys/Create.php | 11 ++- lib/Commands/PHP/Port.php | 15 ++- lib/Commands/PHP/SetDefault.php | 59 +++++++++++ lib/Config.php | 18 +++- lib/ConfigurationCreator.php | 48 +++++++++ lib/Creators/DockerComposeConfiguration.php | 47 ++++----- lib/Creators/NginxConfiguration.php | 69 ++++--------- lib/SupportedPHPVersions.php | 16 +-- studip-docker | 1 + web/LogParser.php | 41 ++++++++ web/assets/script.js | 41 ++++++++ web/assets/style.css | 36 +++++++ web/index.php | 102 +++++++++++++------- 17 files changed, 443 insertions(+), 160 deletions(-) create mode 100644 config/openssl-cert-request create mode 100644 lib/Commands/PHP/SetDefault.php create mode 100644 lib/ConfigurationCreator.php create mode 100644 web/LogParser.php create mode 100644 web/assets/script.js create mode 100644 web/assets/style.css diff --git a/composer.json b/composer.json index 6bc6df8..76ba804 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,8 @@ "name": "tleilax/studip-dockerized", "type": "project", "require": { + "php": "^8", + "ext-json": "*", "symfony/console": "^6.4", "symfony/yaml": "^6.4", "symfony/var-dumper": "^6.4", diff --git a/config/openssl-cert-request b/config/openssl-cert-request new file mode 100644 index 0000000..415c4ff --- /dev/null +++ b/config/openssl-cert-request @@ -0,0 +1,8 @@ +[dn] +CN=localhost +[req] +distinguished_name = dn +[EXT] +subjectAltName=DNS:localhost +keyUsage=digitalSignature +extendedKeyUsage=serverAuth diff --git a/config/php-fpm.conf b/config/php-fpm.conf index 60cf05e..2d2ca09 100644 --- a/config/php-fpm.conf +++ b/config/php-fpm.conf @@ -1,4 +1,4 @@ php_admin_flag[log_errors] = on -access.log = /var/log/php-access.log -php_admin_value[error_log] = /var/log/php-error.log +php_admin_value[error_log] = /var/log/error.log +access.log = /dev/null pm.max_requests = 500 diff --git a/lib/Commands/Compile.php b/lib/Commands/Compile.php index 061a9ee..01fe7e5 100644 --- a/lib/Commands/Compile.php +++ b/lib/Commands/Compile.php @@ -128,12 +128,20 @@ final class Compile extends Command { $creator = new DockerComposeConfiguration('studip-dockerized'); + $creator->hook('services', function (array $service): array { + $service['restart'] = 'unless-stopped'; + return $service; + }); + // Declare volumes $volumes = [ - [realpath($cwd . '/web'), '/var/www/html/studip-dockerized-config.app', 'ro'], - [realpath($cwd . '/config.json'), '/var/www/html/studip-dockerized-config.json', 'ro'], + [realpath($cwd . '/web'), '/usr/share/nginx/html/studip-dockerized-config.app', 'ro'], + [realpath($cwd . '/logs'), '/usr/share/nginx/html/logs', 'ro'], + [realpath($cwd . '/config.json'), '/usr/share/nginx/html/studip-dockerized-config.json', 'ro'], ...array_map( - fn(string $path, array $definition) => [$definition['source'], '/var/www/html/' . $path], + fn(string $path, array $definition) => [ + $definition['source'], '/usr/share/nginx/html/' . $path + ], array_keys(Config::getInstance()->get('sites') ?? []), array_values(Config::getInstance()->get('sites') ?? []) ) @@ -143,23 +151,30 @@ final class Compile extends Command $creator->addService('nginx', 'nginx:alpine', [ 'networks' => ['code-network'], 'depends_on' => ['redis-server', 'memcached-server'], - 'ports' => array_map( - function ($port) { - return "{$port}:{$port}"; - }, - array_values(Config::getInstance()->get('php')) + 'ports' => array_merge( + Config::getInstance()->get('default') ? ['80:80', '443:443'] : [], + ...array_map( + fn($config) => [ + "{$config[0]}:{$config[0]}", + "{$config[1]}:{$config[1]}" + ], + array_values(Config::getInstance()->get('php')) + ), ), ]); $mounts = [ - "{$compiledPath}/nginx.conf" => 'conf.d/nginx.conf', + "{$compiledPath}/nginx.conf" => 'conf.d/default.conf', "{$compiledPath}/nginx-sites.conf" => 'sites.conf', "{$cwd}/config/nginx-php.conf" => 'php.conf', "{$compiledPath}/ssl.crt" => "ssl.crt", "{$compiledPath}/ssl.key" => "ssl.key", ]; foreach ($mounts as $source => $target) { - $creator->addServiceVolume('nginx', $source, "/etc/nginx/{$target}", 'ro'); + if (!str_starts_with($target, '/')) { + $target = "/etc/nginx/{$target}"; + } + $creator->addServiceVolume('nginx', $source, $target, 'ro'); } $creator->addServiceVolume('nginx', realpath("{$cwd}/logs/nginx"), '/var/log/nginx'); @@ -225,25 +240,53 @@ final class Compile extends Command public function createNginxConfiguration(): string { $creator = new NginxConfiguration(); + $creator->hook('server', function (array $config): array { + $config['config'] = [ + ...$config['config'], - // Add upstreams and servers - foreach (Config::getInstance()->get('php') as $version => $port) { + 'server_name _', + + 'proxy_read_timeout' => 300, + 'proxy_connect_timeout' => 300, + 'proxy_send_timeout' => 300, + + 'access_log off', + 'error_log off', + + 'include sites.conf', + ]; + return $config; + }); + + // Add upstreams + $servers = []; + foreach (Config::getInstance()->get('php') as $version => [$port, $sslPort]) { $index = $this->getPHPVersionIndex($version, 'php'); $creator->addUpstream("{$index}_backend", [ "server {$index}:9000" ]); + $servers[] = [$index, $port, $sslPort]; + + if (Config::getInstance()->get('default') === $version) { + $servers[] = [$index, 80, 443]; + } + } + + // Add servers + foreach ($servers as [$index, $port, $sslPort]) { $creator->addServer([ - 'listen ' . $port . ' default_server ssl', - 'server_name _', + 'listen ' . $port . ' default_server', + 'set $fastcgi_backend ' . $index . '_backend', + ]); + + $creator->addServer([ + 'listen ' . $sslPort . ' default_server ssl', + 'set $fastcgi_backend ' . $index . '_backend', 'ssl_certificate /etc/nginx/ssl.crt', 'ssl_certificate_key /etc/nginx/ssl.key', - - 'root /var/www/html', - 'set $fastcgi_backend ' . $index . '_backend', - 'include sites.conf', ]); } @@ -256,15 +299,15 @@ final class Compile extends Command // Add default location $creator->addLocation('/sites', [ - 'alias /var/www/html/studip-dockerized-config.app', + 'alias /usr/share/nginx/html/studip-dockerized-config.app', 'index index.php index.html index.html', - 'set $site_document_root /var/www/html/studip-dockerized-config.app', + 'set $site_document_root /usr/share/nginx/html/studip-dockerized-config.app', 'include php.conf', ]); // Add configured locations foreach (Config::getInstance()->get('sites') ?? [] as $path => $definition) { - $p = '/var/www/html/' . $path; + $p = '/usr/share/nginx/html/' . $path; if (isset($definition['mount'])) { $p .= '/' . $definition['mount']; } diff --git a/lib/Commands/Keys/Create.php b/lib/Commands/Keys/Create.php index dcec238..36dbece 100644 --- a/lib/Commands/Keys/Create.php +++ b/lib/Commands/Keys/Create.php @@ -25,13 +25,16 @@ final class Create extends Command $process = new Process([ 'openssl', 'req', - '-batch', '-x509', + '-out', $this->getCompiledPath('ssl.crt'), + '-keyout', $this->getCompiledPath('ssl.key'), + '-newkey', 'rsa:2048', '-nodes', + '-sha256', '-days', '365', - '-newkey', 'rsa:2048', - '-keyout', $this->getCompiledPath('ssl.key'), - '-out', $this->getCompiledPath('ssl.crt'), + '-subj', '/CN=localhost', + '-extensions', 'EXT', + '-config', realpath(__DIR__ . '/../../../config/openssl-cert-request'), ]); $process->run(); diff --git a/lib/Commands/PHP/Port.php b/lib/Commands/PHP/Port.php index 18cdf70..2a0f6f7 100644 --- a/lib/Commands/PHP/Port.php +++ b/lib/Commands/PHP/Port.php @@ -26,12 +26,19 @@ final class Port extends Command InputArgument::REQUIRED, 'Port to use' ); + + $this->addArgument( + 'ssl-port', + InputArgument::REQUIRED, + 'SSL-Port to use' + ); } protected function execute(InputInterface $input, OutputInterface $output) { $version = $input->getArgument('version'); - $port = $input->getArgument('port'); + $port = (int) $input->getArgument('port'); + $sslPort = (int) $input->getArgument('ssl-port'); $config = Config::getInstance(); $io = new SymfonyStyle($input, $output); @@ -42,12 +49,12 @@ final class Port extends Command } $php = $config->get('php'); - $php[$version] = (int) $port; + $php[$version] = [$port, $sslPort]; $config->set('php', $php); $config->store(); - $io->success("Port of PHP version {$version} was changed to {$port}"); + $io->success("Ports of PHP version {$version} were changed to {$port} / {$sslPort}"); return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/lib/Commands/PHP/SetDefault.php b/lib/Commands/PHP/SetDefault.php new file mode 100644 index 0000000..e700286 --- /dev/null +++ b/lib/Commands/PHP/SetDefault.php @@ -0,0 +1,59 @@ +<?php +namespace Studip\Dockerized\Commands\PHP; + +use Studip\Dockerized\Config; +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; + +final class SetDefault extends Command +{ + protected function configure() + { + $this->setName('php:default'); + $this->setDescription('Set PHP version as default'); + $this->addArgument( + 'version', + InputArgument::REQUIRED, + 'PHP version(s) that should be disabled', + null, + array_keys(Config::getInstance()->get('php')) + ['none'] + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $config = Config::getInstance(); + $io = new SymfonyStyle($input, $output); + + $version = $input->getArgument('version'); + + if ( + $version !== 'none' + && !array_key_exists($version, $config->get('php')) + ) { + $io->error("PHP version {$version} is not enabled"); + return Command::FAILURE; + } + + if ($version === $config->get('default')) { + return Command::SUCCESS; + } + + if ($version === 'none') { + $config->delete('default'); + $config->store(); + + $io->success('Default PHP version has been removed'); + } else { + $config->set('default', $version); + $config->store(); + + $io->success("PHP version {$version} was set as default"); + } + + return Command::SUCCESS; + } +} diff --git a/lib/Config.php b/lib/Config.php index 74cb7db..ace3ee1 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -40,9 +40,11 @@ final class Config return $this->data[$key] ?? null; } - public function set(string $key, mixed $value): void + public function set(string $key, mixed $value): Config { $this->data[$key] = $value; + + return $this; } public function has(string $key): bool @@ -50,9 +52,11 @@ final class Config return array_key_exists($key, $this->data); } - public function delete(string $key): void + public function delete(string $key): Config { unset($this->data[$key]); + + return $this; } public function dump(): string @@ -60,7 +64,7 @@ final class Config return json_encode($this->data, JSON_PRETTY_PRINT); } - public function reset(): void + public function reset(): Config { if (!file_exists($this->filename)) { throw new \Exception("Config file '{$this->filename}' does not exist"); @@ -71,14 +75,18 @@ final class Config } $this->data = json_decode(file_get_contents($this->filename), true); + + return $this; } - public function store(): void + public function store(): Config { if (file_exists($this->filename) && !is_writable($this->filename)) { throw new \Exception("Config file '{$this->filename}' is not writable"); } file_put_contents($this->filename, $this->dump()); + + return $this; } -} \ No newline at end of file +} diff --git a/lib/ConfigurationCreator.php b/lib/ConfigurationCreator.php new file mode 100644 index 0000000..d0b1c78 --- /dev/null +++ b/lib/ConfigurationCreator.php @@ -0,0 +1,48 @@ +<?php +namespace Studip\Dockerized; + +use Closure; +use Exception; + +abstract class ConfigurationCreator +{ + private array $configuration = []; + private array $hooks = []; + + public function hook(string $type, Closure $hook): void + { + if (!isset($this->hooks[$type])) { + $this->hooks[$type] = []; + } + $this->hooks[$type][] = $hook; + } + + public function addConfiguration(string $type, mixed $data, string $key = null): void + { + if (!isset($this->configuration[$type])) { + $this->configuration[$type] = []; + } + if (isset($key)) { + $this->configuration[$type][$key] = $this->applyHooks($type, $data); + } else { + $this->configuration[$type][] = $this->applyHooks($type, $data); + } + } + + protected function getConfiguration(?string $type = null, mixed $default = null): mixed + { + if (func_num_args() === 0 || $type === null) { + return $this->configuration; + } + + return $this->configuration[$type] ?? $default; + } + + protected function applyHooks(string $type, mixed $config): mixed + { + foreach ($this->hooks[$type] ?? [] as $hook) { + $config = $hook($config); + } + return $config; + } +} diff --git a/lib/Creators/DockerComposeConfiguration.php b/lib/Creators/DockerComposeConfiguration.php index 0c494f3..22c8f70 100644 --- a/lib/Creators/DockerComposeConfiguration.php +++ b/lib/Creators/DockerComposeConfiguration.php @@ -1,11 +1,11 @@ <?php namespace Studip\Dockerized\Creators; +use Studip\Dockerized\ConfigurationCreator; use Symfony\Component\Yaml\Yaml; -final class DockerComposeConfiguration +final class DockerComposeConfiguration extends ConfigurationCreator { - private array $configuration = []; private string $cwd; public function __construct(string $cwd, string $name = '') @@ -13,58 +13,45 @@ final class DockerComposeConfiguration $this->cwd = $cwd; if ($name) { - $this->configuration[$name] = $name; + $this->addConfiguration('name', $name); } } public function addService(string $service, ?string $image, array $config = []): void { - if (!isset($this->configuration['services'])) { - $this->configuration['services'] = []; - } - $this->configuration['services'][$service] = array_merge( - isset($image) ? compact('image') : [], + $this->addConfiguration('services', array_merge( + isset($image) ? ['image' => $image] : [], $config - ); - } - - public function extendService(string $service, array $config): void - { - if (!isset($this->configuration['services'][$service])) { - throw new \Exception('Service "' . $service . '" does not exist'); - } - - + ), $service); } public function addServiceVolume(string $service, string $source, string $target, string $modifier = ''): void { - if (!isset($this->configuration['services'][$service])) { + $config = $this->getConfiguration('services', []); + + if (!array_key_exists($service, $config)) { throw new \Exception('Service "' . $service . '" does not exist'); } - if (!isset($this->configuration['services'][$service]['volumes'])) { - $this->configuration['services'][$service]['volumes'] = []; + if (!isset($config[$service]['volumes'])) { + $config[$service]['volumes'] = []; } - - $this->configuration['services'][$service]['volumes'][] = implode(':', array_filter([ + $config[$service]['volumes'][] = implode(':', array_filter([ $source, $target, $modifier, ])); + + $this->addConfiguration('services', $config[$service], $service); } public function addNetwork(string $network, array $config): void { - if (!isset($this->configuration['networks'])) { - $this->configuration['networks'] = []; - } - $this->configuration['networks'][$network] = $config; + $this->addConfiguration('networks', $config, $network); } - public function dump(): string { - return Yaml::dump($this->configuration, 128); + return Yaml::dump($this->getConfiguration(), 128); } -} \ No newline at end of file +} diff --git a/lib/Creators/NginxConfiguration.php b/lib/Creators/NginxConfiguration.php index 9e4c710..ffcadca 100644 --- a/lib/Creators/NginxConfiguration.php +++ b/lib/Creators/NginxConfiguration.php @@ -2,58 +2,32 @@ namespace Studip\Dockerized\Creators; use RomanPitak\Nginx\Config\Scope; +use Studip\Dockerized\ConfigurationCreator; -final class NginxConfiguration +final class NginxConfiguration extends ConfigurationCreator { - public const EVENT_LOCATION_ADD_BEFORE = 'location:add:before'; - public const EVENT_SERVER_ADD_BEFORE = 'server:add:before'; - public const EVENT_UPSTREAM_ADD_BEFORE = 'upstream:add:before'; - - private array $locations = []; - private array $servers = []; - private array $upstreams = []; - - private array $event_handlers = []; - - public function __construct() - { - } - - public function on(string $event, \Closure $handler): void - { - if (!isset($this->event_handlers[$event])) { - $this->event_handlers[$event] = []; - } - $this->event_handlers[$event][] = $handler; - } - - private function trigger(string $event, &...$data): void - { - if (isset($this->event_handlers[$event])) { - foreach ($this->event_handlers[$event] as $handler) { - $handler(...$data); - } - } - } - public function addLocation(string $location, array $config, string $modifier = '^~') { - $location = '/' . ltrim($location, '/'); - - $this->trigger(self::EVENT_LOCATION_ADD_BEFORE, $location, $config, $modifier); - $this->locations[] = compact('location', 'modifier', 'config'); + $this->addConfiguration('location', [ + 'location' => $location = '/' . ltrim($location, '/'), + 'modifier' => $modifier, + 'config' => $config, + ]); } public function addServer(array $config) { - $this->trigger(self::EVENT_SERVER_ADD_BEFORE, $config); - $this->servers[] = compact('config'); + $this->addConfiguration('server', [ + 'config' => $config, + ]); } public function addUpstream(string $upstream, array $config) { - $this->trigger(self::EVENT_UPSTREAM_ADD_BEFORE, $upstream, $config); - $this->upstreams[] = compact('upstream', 'config'); + $this->addConfiguration('upstream', [ + 'upstream' => $upstream, + 'config' => $config, + ]); } public function dump(): string @@ -65,21 +39,21 @@ final class NginxConfiguration "upstream {$upstream['upstream']}", $upstream['config'] ), - $this->upstreams + $this->getConfiguration('upstream', []) ), ...array_map( fn($server) => $this->write( 'server', $server['config'] ), - $this->servers + $this->getConfiguration('server', []) ), ...array_map( fn($location) => $this->write( "location {$location['modifier']} {$location['location']}", $location['config'] ), - $this->locations + $this->getConfiguration('location', []) ), ])); $result = (string) Scope::fromFile($tempName); @@ -88,12 +62,7 @@ final class NginxConfiguration return $result; } - public function __toString(): string - { - return $this->dump(); - } - - public function write(string $index, array $content): string + private function write(string $index, array $content): string { return sprintf( '%s { %s }', @@ -104,4 +73,4 @@ final class NginxConfiguration )) ); } -} \ No newline at end of file +} diff --git a/lib/SupportedPHPVersions.php b/lib/SupportedPHPVersions.php index 4db0926..1b70aee 100644 --- a/lib/SupportedPHPVersions.php +++ b/lib/SupportedPHPVersions.php @@ -4,13 +4,13 @@ namespace Studip\Dockerized; final class SupportedPHPVersions { private const CONFIGURATION = [ - '7.2' => 8072, - '7.3' => 8073, - '7.4' => 8074, - '8.0' => 8080, - '8.1' => 8081, - '8.2' => 8082, - '8.3' => 8083, + '7.2' => [8072, 18072], + '7.3' => [8073, 18073], + '7.4' => [8074, 18074], + '8.0' => [8080, 18080], + '8.1' => [8081, 18081], + '8.2' => [8082, 18082], + '8.3' => [8083, 18083], ]; private const DEFAULT = ['7.4', '8.1', '8.3']; @@ -32,4 +32,4 @@ final class SupportedPHPVersions } return self::CONFIGURATION[$version]; } -} \ No newline at end of file +} diff --git a/studip-docker b/studip-docker index aa12677..7f8162f 100755 --- a/studip-docker +++ b/studip-docker @@ -20,6 +20,7 @@ if (file_exists(CONFIG_FILE)) { $application->add(new Commands\Sites\Remove()); $application->add(new Commands\Sites\Show()); + $application->add(new Commands\PHP\SetDefault()); $application->add(new Commands\PHP\Disable()); $application->add(new Commands\PHP\Enable()); $application->add(new Commands\PHP\Port()); diff --git a/web/LogParser.php b/web/LogParser.php new file mode 100644 index 0000000..48fad38 --- /dev/null +++ b/web/LogParser.php @@ -0,0 +1,41 @@ +<?php +final class LogParser +{ + private string $filename; + + public function __construct(string $filename) + { + $this->filename = $filename; + } + + public function hasData(): bool + { + return file_exists($this->filename) + && filesize($this->filename) > 0; + } + + public function entries(): array + { + $entries = []; + $index = -1; + + $fp = fopen($this->filename, 'r'); + while (!feof($fp)) { + $line = trim(fgets($fp)); + if (strlen($line) === 0) { + continue; + } + + if ($index === -1 || preg_match('/^\[.+]/', $line)) { + $index += 1; + } + + if (!isset($entries[$index])) { + $entries[$index] = ''; + } + $entries[$index] .= $entries[$index] ? "\n" . $line : $line; + } + + return array_reverse($entries); + } +} diff --git a/web/assets/script.js b/web/assets/script.js new file mode 100644 index 0000000..cd60e28 --- /dev/null +++ b/web/assets/script.js @@ -0,0 +1,41 @@ +const ready = new Promise(resolve => { + if (document.readyState === 'complete') { + resolve(); + } else { + window.addEventListener('DOMContentLoaded', () => { + resolve(); + }); + } +}); + +ready.then(() => { + // const tabs = document.querySelector('.tabs'); + // + // // Change default selected if hash is set + // if (window.location.hash) { + // tabs.childNodes.forEach(node => { + // node.classList.toggle('active', node.href === window.location.hash); + // }); + // } + // + // const selectors = Array.from(tabs.querySelectorAll('a')).map(node => { + // const selector = new URL(node.href).hash; + // + // node.addEventListener('click', (event) => { + // tabs.querySelector('.active').classList.remove('active'); + // node.parentNode.classList.toggle('active'); + // + // selectors.forEach(s => { + // document.querySelector(s).toggleAttribute('hidden', s !== selector) + // }); + // + // event.preventDefault(); + // }); + // + // if (!node.parentNode.classList.contains('active')) { + // document.querySelector(selector).setAttribute('hidden', ''); + // } + // + // return selector; + // }); +}) diff --git a/web/assets/style.css b/web/assets/style.css new file mode 100644 index 0000000..07e712f --- /dev/null +++ b/web/assets/style.css @@ -0,0 +1,36 @@ +.tabs { + background-color: #fff; + border-bottom: 1px solid #ccc; + font-size: 1.5em; + line-height: 1.5em; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 1.5em; + padding: 0 1em; +} +.tabs a { + text-decoration: none; +} +.tabs a:focus { + font-weight: bold; +} + +section { + padding-top: 1.5em; +} +section:not(:target) { + display: none; +} + +table { + font-size: 3.5em; + width: 100%; +} +th { + text-align: left; +} +tr:hover td { + background-color: #fec; +} diff --git a/web/index.php b/web/index.php index 5fdebf2..a238c72 100644 --- a/web/index.php +++ b/web/index.php @@ -1,48 +1,78 @@ <?php +require_once __DIR__ . '/LogParser.php'; + $config = json_decode(file_get_contents(__DIR__ . '/../studip-dockerized-config.json'), true); +$versions = $config['php']; +ksort($versions); + +$log = new LogParser(__DIR__ . '/../logs/php/error.log'); ?> <!doctype html> <html> <head> - + <link href="assets/style.css" type="text/css" rel="stylesheet"> + <script src="assets/script.js"></script> + <title>Available sites</title> </head> <body> -<style> -html { - font-size: 4em; -} -table { - width: 100%; -} -th { - text-align: left; -} -</style> -<?php if (empty($config['sites'])): ?> - No sites defined -<?php else: ?> - <table> - <thead> - <tr> - <th>Name</th> - <th colspan="<?= count($config['php']) ?>"></th> - </tr> - </thead> - <tbody> - <?php foreach ($config['sites'] ?? [] as $path => $definition): ?> - <tr> - <td><?= htmlentities($path) ?></td> - <?php foreach ($config['php'] as $version => $port): ?> - <td> - <a href="https://localhost:<?= $port ?>/<?= htmlentities($path) ?>"> - <?= htmlentities($version) ?> - </a> - </td> + <nav class="tabs"> + <span class="active"> + <a href="#sites">Sites</a> + </span> + <span> + <a href="#logs">Logs</a> + </span> + </nav> + + <section id="sites"> + <?php if (empty($config['sites'])): ?> + No sites defined + <?php else: ?> + <table> + <thead> + <tr> + <th>Name</th> + <th colspan="<?= count($config['php']) ?>"></th> + </tr> + </thead> + <tbody> + <?php foreach ($config['sites'] ?? [] as $path => $definition): ?> + <tr> + <td> + <?php if (!empty($config['default'])): ?> + <a href="https://localhost/<?= htmlentities($path) ?>"> + <?= htmlentities($path) ?> + </a> + <?php else: ?> + <?= htmlentities($path) ?> + <?php endif; ?> + </td> + <?php foreach ($versions as $version => [$port, $sslPort]): ?> + <td> + <a href="https://localhost:<?= $port ?>/<?= htmlentities($path) ?>"> + <?= htmlentities($version) ?> + </a> + <a href="https://localhost:<?= $sslPort ?>/<?= htmlentities($path) ?>"> + (SSL) + </a> + </td> + <?php endforeach; ?> + </tr> <?php endforeach; ?> - </tr> + </tbody> + </table> + <?php endif; ?> + + </section> + + <section id="logs"> + <?php if ($log->hasData()): ?> + <ul> + <?php foreach ($log->entries() as $entry): ?> + <li><?= nl2br(htmlentities($entry)) ?></li> <?php endforeach; ?> - </tbody> - </table> -<?php endif; ?> + </ul> + <?php endif; ?> + </section> </body> </html> -- GitLab