diff --git a/src/Game/Controller/GameAdminController.php b/src/Game/Controller/GameAdminController.php index 33877fa..20efaa3 100644 --- a/src/Game/Controller/GameAdminController.php +++ b/src/Game/Controller/GameAdminController.php @@ -54,11 +54,10 @@ final class GameAdminController extends AbstractController { $playersLogs = []; foreach ($session->getPlayers() as $player) { - $username = $player->getUser()->getUsername(); - $logFile = $this->projectDir . '/var/log/sessions/' . $session->getId() . '/' . $username . '.txt'; + $logFile = $this->projectDir . '/var/log/sessions/' . $session->getId() . '/' . $player->getLogFileBasename() . '.txt'; $playersLogs[] = [ - 'username' => $username, + 'username' => $player->getUser()->getUsername(), 'logs' => file_exists($logFile) ? file_get_contents($logFile) : '', ]; } diff --git a/src/Game/Entity/Player.php b/src/Game/Entity/Player.php index a99bd7c..888f500 100644 --- a/src/Game/Entity/Player.php +++ b/src/Game/Entity/Player.php @@ -66,4 +66,19 @@ class Player return $this; } + + /** + * A filesystem-safe basename derived from the player's username, for use when + * building per-player log file paths. Usernames are validated to only contain + * safe characters at registration time, but this sanitizes defensively too, so + * a path segment can never traverse outside its intended directory regardless + * of what ends up stored on the user. + */ + public function getLogFileBasename(): string + { + $username = $this->user?->getUsername() ?? ''; + $safe = preg_replace('/[^A-Za-z0-9_-]/', '_', $username); + + return $safe !== null && $safe !== '' ? $safe : ('player-' . $this->id); + } } diff --git a/src/Game/Service/GameResponseService.php b/src/Game/Service/GameResponseService.php index c01cdfa..280601d 100644 --- a/src/Game/Service/GameResponseService.php +++ b/src/Game/Service/GameResponseService.php @@ -107,14 +107,13 @@ class GameResponseService private function logSessionActivity(Player $player, string $content): void { $sessionId = $player->getSession()->getId(); - $username = $player->getUser()->getUsername(); $logDir = $this->projectDir . '/var/log/sessions/' . $sessionId; if (!is_dir($logDir)) { mkdir($logDir, 0777, true); } - $logFile = $logDir . '/' . $username . '.txt'; + $logFile = $logDir . '/' . $player->getLogFileBasename() . '.txt'; $timestamp = date('Y-m-d H:i:s'); $logMessage = sprintf("[%s] %s\n", $timestamp, $content); diff --git a/src/Tech/Form/AdminUserType.php b/src/Tech/Form/AdminUserType.php index d701d87..7d2be2e 100644 --- a/src/Tech/Form/AdminUserType.php +++ b/src/Tech/Form/AdminUserType.php @@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; class AdminUserType extends AbstractType { @@ -26,7 +27,14 @@ class AdminUserType extends AbstractType 'constraints' => [new NotBlank(), new Email()], ]) ->add('username', TextType::class, [ - 'constraints' => [new NotBlank(), new Length(min: 2, max: 180)], + 'constraints' => [ + new NotBlank(), + new Length(min: 2, max: 32), + new Regex( + pattern: '/^[A-Za-z0-9_-]+$/', + message: 'Username may only contain letters, numbers, underscores, and hyphens.', + ), + ], ]) ->add('plainPassword', PasswordType::class, [ 'mapped' => false, diff --git a/src/Tech/Form/RegistrationFormType.php b/src/Tech/Form/RegistrationFormType.php index daabb19..e78a4f8 100644 --- a/src/Tech/Form/RegistrationFormType.php +++ b/src/Tech/Form/RegistrationFormType.php @@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\IsTrue; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; class RegistrationFormType extends AbstractType { @@ -26,6 +27,11 @@ class RegistrationFormType extends AbstractType ->add('username', TextType::class, [ 'constraints' => [ new NotBlank(message: 'Please enter a username'), + new Length(min: 3, max: 32, minMessage: 'Your username should be at least {{ limit }} characters', maxMessage: 'Your username cannot be longer than {{ limit }} characters'), + new Regex( + pattern: '/^[A-Za-z0-9_-]+$/', + message: 'Your username may only contain letters, numbers, underscores, and hyphens.', + ), ], ]) ->add('plainPassword', RepeatedType::class, [ diff --git a/tests/Game/SessionLoggingTest.php b/tests/Game/SessionLoggingTest.php index 5b52ee9..4e7e5fa 100644 --- a/tests/Game/SessionLoggingTest.php +++ b/tests/Game/SessionLoggingTest.php @@ -74,6 +74,7 @@ class SessionLoggingTest extends TestCase $player->method('getUser')->willReturn($user); $player->method('getSession')->willReturn($session); $player->method('getScreen')->willReturn(1); + $player->method('getLogFileBasename')->willReturn('player1'); $this->security->method('getUser')->willReturn($user); $this->playerService->method('GetCurrentlyActiveAsPlayer')->willReturn($player);