Registration only validated username with NotBlank, so a username like "../../../../public/x" got concatenated directly into a filesystem path for both writing (game activity logs) and reading (admin log viewer) - reachable from the public webroot since public/ is a few directories up from where those logs are stored. Adds a character-set validator (letters, numbers, underscore, hyphen) to registration and admin user editing going forward, and sanitizes at the point of use (Player::getLogFileBasename()) so any already-stored unsafe username can't escape the log directory either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
72 lines
2.6 KiB
PHP
72 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Game\Controller;
|
|
|
|
use App\Game\Entity\Session;
|
|
use App\Game\Enum\SessionStatus;
|
|
use App\Game\Repository\GameRepository;
|
|
use App\Game\Repository\LobbyMessageRepository;
|
|
use App\Game\Repository\SessionRepository;
|
|
use App\Tech\Repository\UserRepository;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Annotation\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
|
|
#[Route('/admin')]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
final class GameAdminController extends AbstractController
|
|
{
|
|
public function __construct(
|
|
#[Autowire('%kernel.project_dir%')]
|
|
private string $projectDir
|
|
) {
|
|
}
|
|
|
|
#[Route('', name: 'game_admin_dashboard', methods: ['GET'])]
|
|
public function index(
|
|
UserRepository $userRepository,
|
|
SessionRepository $sessionRepository,
|
|
GameRepository $gameRepository,
|
|
): Response {
|
|
$allUsers = $userRepository->findAll();
|
|
$allSessions = $sessionRepository->findAll();
|
|
$activeSessions = array_filter(
|
|
$allSessions,
|
|
fn(Session $s) => in_array($s->getStatus(), [SessionStatus::CREATED, SessionStatus::READY, SessionStatus::PLAYING])
|
|
);
|
|
|
|
return $this->render('game/admin/index.html.twig', [
|
|
'totalUsers' => count($allUsers),
|
|
'totalPlayers' => count($userRepository->findByRole('ROLE_PLAYER')),
|
|
'totalAdmins' => count($userRepository->findByRole('ROLE_ADMIN')),
|
|
'totalGames' => count($gameRepository->findAll()),
|
|
'totalSessions' => count($allSessions),
|
|
'activeSessions' => count($activeSessions),
|
|
]);
|
|
}
|
|
|
|
#[Route('/session/{session}', name: 'game_admin_view_session', methods: ['GET'])]
|
|
public function viewSession(Session $session, LobbyMessageRepository $lobbyMessageRepository): Response
|
|
{
|
|
$playersLogs = [];
|
|
foreach ($session->getPlayers() as $player) {
|
|
$logFile = $this->projectDir . '/var/log/sessions/' . $session->getId() . '/' . $player->getLogFileBasename() . '.txt';
|
|
|
|
$playersLogs[] = [
|
|
'username' => $player->getUser()->getUsername(),
|
|
'logs' => file_exists($logFile) ? file_get_contents($logFile) : '',
|
|
];
|
|
}
|
|
|
|
return $this->render('game/admin/sessions/view.html.twig', [
|
|
'session' => $session,
|
|
'playersLogs' => $playersLogs,
|
|
'lobbyMessages' => $lobbyMessageRepository->findForSession($session),
|
|
]);
|
|
}
|
|
}
|