From a7f2697a34895f8761129d8282102090e19b5539 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Sun, 26 Jul 2026 07:28:26 -0700 Subject: [PATCH 1/2] Cache must be purged when unmounting extensions --- src/QuickInstall/Sandbox/BoardRunner.php | 2 ++ tests/Unit/BoardRunnerTest.php | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/QuickInstall/Sandbox/BoardRunner.php b/src/QuickInstall/Sandbox/BoardRunner.php index 1564c15a..1066892b 100644 --- a/src/QuickInstall/Sandbox/BoardRunner.php +++ b/src/QuickInstall/Sandbox/BoardRunner.php @@ -161,6 +161,8 @@ public function uninstallExtension(string $board, string $name): void } $this->run(array_merge($command, ['extension:purge', $name])); + // Drop compiled autoloaders while the extension files are still mounted. + $this->purgeCache($board); } /** Runs phpBB-aware style cleanup before its files disappear. */ diff --git a/tests/Unit/BoardRunnerTest.php b/tests/Unit/BoardRunnerTest.php index 0cdb5d78..4938c2eb 100644 --- a/tests/Unit/BoardRunnerTest.php +++ b/tests/Unit/BoardRunnerTest.php @@ -79,7 +79,7 @@ public function testStopPurgeCacheAndRecreateWebRunExpectedDockerCommands(): voi self::assertSame(['up', '-d', '--force-recreate', 'web'], array_slice($runner->runs[2], -4)); } - public function testUninstallExtensionDisablesThenPurges(): void + public function testUninstallExtensionDisablesPurgesAndClearsCache(): void { [$project] = $this->projectWithBoard(); $runner = new TestBoardRunner($project); @@ -89,6 +89,7 @@ public function testUninstallExtensionDisablesThenPurges(): void self::assertSame(['extension:disable', 'vendor/demo'], array_slice($runner->capturedCommands[0], -2)); self::assertSame(['extension:purge', 'vendor/demo'], array_slice($runner->runs[0], -2)); + self::assertSame('php bin/phpbbcli.php cache:purge', end($runner->runs[1])); } public function testUninstallStyleCopiesAndRunsPhpbbCleanupScript(): void From b405cbcc81e0f1e59647420beb70901905aef895 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Sun, 26 Jul 2026 07:45:14 -0700 Subject: [PATCH 2/2] Set created board timezones to the local host tz --- .../Sandbox/BoardRefreshService.php | 1 + src/QuickInstall/Sandbox/BoardService.php | 49 +++++++++++++++++++ .../Sandbox/DockerComposeWriter.php | 7 +++ tests/Unit/BoardServiceTest.php | 30 ++++++++++++ tests/Unit/DockerComposeWriterTest.php | 9 +++- 5 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/QuickInstall/Sandbox/BoardRefreshService.php b/src/QuickInstall/Sandbox/BoardRefreshService.php index 330264f7..87452cd5 100644 --- a/src/QuickInstall/Sandbox/BoardRefreshService.php +++ b/src/QuickInstall/Sandbox/BoardRefreshService.php @@ -45,6 +45,7 @@ private function runtimeConfig(array $board): array 'admin_pass' => 'password', 'admin_email' => 'admin@example.test', 'board_email' => 'board@example.test', + 'board_timezone' => 'UTC', 'populate' => 'none', 'debug' => false, 'extensions' => [], diff --git a/src/QuickInstall/Sandbox/BoardService.php b/src/QuickInstall/Sandbox/BoardService.php index 409a40aa..c27d4e50 100644 --- a/src/QuickInstall/Sandbox/BoardService.php +++ b/src/QuickInstall/Sandbox/BoardService.php @@ -92,6 +92,7 @@ public function create(string $name, string $version = 'latest', string $db = 'm 'admin_pass' => 'password', 'admin_email' => 'admin@example.test', 'board_email' => 'board@example.test', + 'board_timezone' => $this->hostTimezone(), 'extensions' => [], 'styles' => [], ]; @@ -124,6 +125,7 @@ public function create(string $name, string $version = 'latest', string $db = 'm 'path' => $boardDir, 'populate' => $populate, 'debug' => $debug, + 'board_timezone' => $config['board_timezone'], 'extensions' => [], 'styles' => [], 'created_at' => gmdate('c'), @@ -142,6 +144,53 @@ public function create(string $name, string $version = 'latest', string $db = 'm return ['board' => $board, 'paths' => $paths]; } + /** Returns the host's PHP-supported timezone identifier, falling back to UTC. */ + protected function hostTimezone(): string + { + $candidates = []; + $environment = getenv('TZ'); + if (is_string($environment)) + { + $candidates[] = ltrim(trim($environment), ':'); + } + + $localtime = realpath('/etc/localtime'); + if (is_string($localtime) && preg_match('#/zoneinfo/(.+)$#', $localtime, $match)) + { + $candidates[] = $match[1]; + } + + if (is_readable('/etc/timezone')) + { + $timezone = file_get_contents('/etc/timezone'); + if (is_string($timezone)) + { + $candidates[] = trim($timezone); + } + } + + $candidates[] = date_default_timezone_get(); + foreach ($candidates as $candidate) + { + if ($candidate === '') + { + continue; + } + + try + { + // Match phpBB's own timezone validation semantics. + new \DateTimeZone($candidate); + return $candidate; + } + catch (\Exception $e) + { + } + } + + return 'UTC'; + } + private function backupBoardState(string $name): array { $token = str_replace('.', '', uniqid('replace-', true)); diff --git a/src/QuickInstall/Sandbox/DockerComposeWriter.php b/src/QuickInstall/Sandbox/DockerComposeWriter.php index 52a3f83d..70d38b96 100644 --- a/src/QuickInstall/Sandbox/DockerComposeWriter.php +++ b/src/QuickInstall/Sandbox/DockerComposeWriter.php @@ -143,6 +143,7 @@ private function compose(string $name, array $config): string environment: QUICKINSTALL_PHPBB_VERSION: "{$config['phpbb']}" QUICKINSTALL_POPULATE: "{$config['populate']}" + QUICKINSTALL_BOARD_TIMEZONE: "{$config['board_timezone']}" $dbService @@ -274,6 +275,12 @@ private function entrypoint(): string if [ ! -s /var/www/html/config.php ] && [ -f /var/www/html/install/phpbbcli.php ]; then php /var/www/html/install/phpbbcli.php install /opt/quickinstall/install-config.yml rm -rf /var/www/html/install + if php -r 'try { new DateTimeZone((string) getenv("QUICKINSTALL_BOARD_TIMEZONE")); } catch (Exception $e) { exit(1); }'; then + php /var/www/html/bin/phpbbcli.php config:set board_timezone "$QUICKINSTALL_BOARD_TIMEZONE" + else + echo "Host timezone '$QUICKINSTALL_BOARD_TIMEZONE' is unsupported by this PHP runtime; using UTC." + php /var/www/html/bin/phpbbcli.php config:set board_timezone UTC + fi chown -R www-data:www-data /var/www/html fi diff --git a/tests/Unit/BoardServiceTest.php b/tests/Unit/BoardServiceTest.php index 43c3bc63..1b2a9a2f 100644 --- a/tests/Unit/BoardServiceTest.php +++ b/tests/Unit/BoardServiceTest.php @@ -27,10 +27,27 @@ public function testCreateWritesBoardAndRuntimeConfig(): void self::assertSame(8090, $result['board']['port']); self::assertSame('tiny', $result['board']['populate']); self::assertTrue($result['board']['debug']); + self::assertSame('America/Los_Angeles', $result['board']['board_timezone']); self::assertSame('demo', $project->boards()['demo']['name']); self::assertFileExists($project->composePath('demo')); } + public function testHostTimezoneUsesPhpCompatibleEnvironmentValue(): void + { + $previous = getenv('TZ'); + putenv('TZ=US/Pacific'); + + try + { + $project = $this->projectWithSource('3.3.14'); + self::assertSame('US/Pacific', (new HostTimezoneTestBoardService($project))->detectedHostTimezone()); + } + finally + { + $previous === false ? putenv('TZ') : putenv('TZ=' . $previous); + } + } + public function testCreateRejectsDuplicateWithoutReplace(): void { $project = $this->projectWithSource('3.3.14'); @@ -236,6 +253,14 @@ private function projectWithSource(string $version, ?string $osFamily = null): P } } +class HostTimezoneTestBoardService extends BoardService +{ + public function detectedHostTimezone(): string + { + return $this->hostTimezone(); + } +} + class TestBoardService extends BoardService { private ?ServiceTestBoardRunner $runner; @@ -255,6 +280,11 @@ protected function isPortInUse(int $port): bool return $this->portInUse; } + protected function hostTimezone(): string + { + return 'America/Los_Angeles'; + } + protected function createBoardRunner(): BoardRunner { return $this->runner ?? parent::createBoardRunner(); diff --git a/tests/Unit/DockerComposeWriterTest.php b/tests/Unit/DockerComposeWriterTest.php index ac7b6aec..942ee51f 100644 --- a/tests/Unit/DockerComposeWriterTest.php +++ b/tests/Unit/DockerComposeWriterTest.php @@ -22,7 +22,13 @@ public function testWritesDatabaseRuntimeFiles(string $board, string $db, array self::assertFileExists($paths['install_config']); self::assertFileExists($paths['dockerfile']); self::assertFileExists($paths['entrypoint']); - self::assertStringContainsString('apache2-foreground', file_get_contents($paths['entrypoint'])); + $entrypoint = file_get_contents($paths['entrypoint']); + self::assertStringContainsString('apache2-foreground', $entrypoint); + self::assertStringContainsString('new DateTimeZone((string) getenv("QUICKINSTALL_BOARD_TIMEZONE"))', $entrypoint); + self::assertStringContainsString('config:set board_timezone "$QUICKINSTALL_BOARD_TIMEZONE"', $entrypoint); + self::assertStringContainsString('config:set board_timezone UTC', $entrypoint); + self::assertStringContainsString("is unsupported by this PHP runtime; using UTC.", $entrypoint); + self::assertStringContainsString('QUICKINSTALL_BOARD_TIMEZONE: "America/Los_Angeles"', file_get_contents($paths['compose'])); $output = file_get_contents($paths['compose']) . "\n" . file_get_contents($paths['install_config']) . "\n" . file_get_contents($paths['dockerfile']); foreach ($expectedContains as $expected) @@ -164,6 +170,7 @@ private function config(array $overrides = []): array 'admin_pass' => 'password', 'admin_email' => 'admin@example.test', 'board_email' => 'board@example.test', + 'board_timezone' => 'America/Los_Angeles', 'extensions' => [ 'acme/demo' => ['mode' => 'bind', 'source' => '/tmp/acme-demo'], ],