<?php
namespace App\Controller;
use App\Service\Mailer;
use App\Service\Notifier;
use App\Entity\Notification;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Service\Attribute\Required;
#[Route("/dev")]
class DevController extends AbstractController
{
const DUMP_PATH = '../var/dump/';
#[Required]
public KernelInterface $kernel;
#[Required]
public Filesystem $filesystem;
#[Required]
public EntityManagerInterface $entityManager;
/**
* Contrôles :
* - Intégrité de la base de donnée : projets/porteurs orphelins ?
* - Post import : projets/porteurs incomplet ?
* - Chmod des dossiers principaux..
*/
#[Route("/checkup", name: "dev.data.checkup")]
public function controls()
{
$this->secureIfProd();
// Remonté des config php
$phps = [
'memory_limit' => false,
'upload_max_filesize' => false,
'post_max_size' => false,
'max_execution_time' => false,
'max_input_time' => false,
'disable_functions' => false
];
foreach($phps as $php => $state){
$phps[$php] = ini_get($php);
}
// Check des droits d'écriture
$dirs = [
'var' => false,
'var/export' => false,
'var/yarn' => false,
'var/log' => false,
'var/dump' => false,
'var/cache' => false,
'var/upload' => false,
'public/upload' => false,
];
foreach($dirs as $dir => $state){
$dirs[$dir] = is_dir('../'.$dir) && is_writable('../'.$dir);
}
return $this->render('dev/data/controls.html.twig', [
'infoPhp' => $phps,
'infoChmod' => $dirs,
]);
}
/**
* Listing complet des utilisateurs
*/
#[Route("/data", name: "dev.data.users")]
public function users(EntityManagerInterface $em)
{
$this->secureIfProd();
/** @var UserRepository $userRepository */
$userRepository = $em->getRepository(User::class);
$roles = $this->getParameter('security.role_hierarchy.roles');
$users = [];
foreach($roles as $role) {
$role = $role[0];
if ($role === 'ROLE_USER') {
continue;
}
$users[$role] = $userRepository->findAllByRolesOrderByStructure([$role]);
}
// Limiter à 1000 utilisateurs par rôle
$rolesTruncated = []; // Afficher un message au niveau des rôles correspondants
$maxUsersDisplayed = 1000; // Nombre maximum
foreach($users as $role => $usersHavingRole) {
if (count($usersHavingRole) > $maxUsersDisplayed) {
$users[$role] = array_slice($usersHavingRole, 0, $maxUsersDisplayed);
$rolesTruncated []= $role;
}
}
$git = [
'time' => file_exists('../.git/index') ? filectime('../.git/index') : 0,
'hash' => file_exists('../.git/ORIG_HEAD') ? file_get_contents('../.git/ORIG_HEAD') : '',
];
return $this->render('dev/data/users.html.twig', [
'usersByRole' => $users,
'infoGit' => $git,
'rolesTruncated' => $rolesTruncated,
'maxUsersDisplayed' => $maxUsersDisplayed,
]);
}
/**
* Permet de se connecter "en tant que"..
*/
#[Route("/login/{id}", name: "dev.data.login")]
public function login($id, EntityManagerInterface $em)
{
$this->secureIfProd();
$user = $em->getRepository(User::class)->find($id);
if ($user) {
/** @var User $user */
$providerKey = 'main'; // La clé du firewall
$token = new UsernamePasswordToken($user, $user->getPassword(), $providerKey, $user->getRoles());
$this->get('security.token_storage')->setToken($token);
$this->get('session')->set('_security_' . $providerKey, serialize($token));
// N'existe plus ?
// $event = new InteractiveLoginEvent($request, $token);
// $dispatcher->dispatch('security.interactive_login', $event);
$this->addFlash('success','Désormais connecté en tant que "' . $user->getEmail() . '", rôle "' . implode(',', $user->getRoles()) . '"');
return $this->redirectToRoute('home');
}
$this->addFlash('warning', 'Utilisateur introuvable..');
return $this->redirectToRoute('dev.data.users');
}
/**
* Gestion des bases de données
*/
#[Route("/dbmanager", name: "dev.data.dbmanager")]
public function dbmanager(): Response
{
$this->secureIfProd();
$dumps = new Finder();
$dumps->sortByModifiedTime();
$dumps->reverseSorting();
if(!file_exists(self::DUMP_PATH)) {
mkdir(self::DUMP_PATH);
}
$files = [];
foreach ($dumps->files()->in(self::DUMP_PATH) as $dump) {
if(preg_match('/.lock$/', $dump->getFilename())) {
continue;
}
if ($this->filesystem->exists(self::DUMP_PATH . $dump->getFilename() . '.lock')) {
$import = new Finder();
$import->name($dump->getFilename() . '.lock');
foreach ($import->in(self::DUMP_PATH) as $file) {
$dump->import = (New \DateTime())->setTimestamp($file->getATime())->format('d/m/Y \\à H:i');
}
} else {
$dump->import = 'jamais';
}
$dump->filesize = $this->filesize($dump->getSize());
array_push($files, $dump);
}
$logs = [];
$dumps = new Finder();
$dumps->sortByModifiedTime();
$dumps->reverseSorting();
foreach ($dumps->files()->in('../var/log') as $log) {
$log->filesize = $this->filesize($log->getSize());
array_push($logs, $log);
}
return $this->render('dev/data/dbmanager.html.twig',[
'dumps' => $files,
'logs' => $logs,
'dbinfo' => $this->getDoctrine()->getConnection('default')->getParams()
]);
}
/**
* Créer un dump
*/
#[Route("/dbmanager/dump", name: "dev.data.dbmanager.dump")]
public function dump(Request $request): Response
{
$this->secureIfProd();
if ($request->getMethod() === 'GET') {
$form = $this->createFormBuilder()
->add('name', TextType::class, [
'label' => 'Nom du dump',
'required' => true
])
->getForm();
if ($request->isXmlHttpRequest()) {
return $this->render('dev/data/partials/_create_dump.html.twig', [
'form' => $form->createView()
]);
}
}
if ($request->getMethod() === 'POST') {
$form = $this->createFormBuilder()
->setMethod('POST')
->add('name', TextType::class, [
'label' => 'Nom du dump',
'required' => true
])
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$name = strtolower(str_replace(' ', '_', $request->request->get('form')['name']));
$filename = self::DUMP_PATH . $name . '.sql';
if ($this->filesystem->exists($filename)) {
$this->addFlash('icon@danger', 'Le nom de fichier existe déjà');
return $this->redirectToRoute('dev.data.dbmanager');
}
$cfg = $this->getDoctrine()->getConnection('default')->getParams();
try {
passthru('mysqldump -h '.$cfg['host'].' -u'.$cfg['user'].' -p'.$cfg['password'].' '.$cfg['dbname'].' > '.$filename);
if ($this->filesystem->exists($filename)) {
$this->addFlash('icon@success', 'Dump effectué avec succès.');
}
} catch (Exception $exception){
$this->addFlash('icon@danger', "Une erreur s'est produite.. : ".$exception->getMessage());
}
}
}
return $this->redirectToRoute('dev.data.dbmanager');
}
/**
* Gestion des bases de données
*/
#[Route("/dbmanager/download/{filename}", name: "dev.data.dbmanager.download")]
public function downloadDB(string $filename): Response
{
$this->secureIfProd();
$filename = self::DUMP_PATH . $filename;
if ($this->filesystem->exists($filename)) {
return $this->file($filename);
}
$this->addFlash('icon@danger', 'Fichier introuvable');
return $this->redirectToRoute('dev.data.dbmanager');
}
/**
* Téléchargement d'un fichier de log
*/
#[Route("/dbmanager/log/download/{filename}", name: "dev.data.log.download")]
public function downloadLogFile(string $filename): Response
{
$this->secureIfProd();
$pathFile = '../var/log/' . $filename;
if ($this->filesystem->exists($pathFile) && preg_match('/^[0-9a-z\-\_]+\.log$/', $filename)) {
return $this->file($pathFile);
}
$this->addFlash('icon@danger', 'Fichier introuvable');
return $this->redirectToRoute('dev.data.dbmanager');
}
/**
* Suppression d'un fichier de log
*/
#[Route("/dbmanager/log/remove/{filename}", name: "dev.data.log.remove")]
public function removeLogFile(string $filename): Response
{
$this->secureIfProd();
$pathFile = '../var/log/' . $filename;
if ($this->filesystem->exists($pathFile) && preg_match('/^[0-9a-z\-\_]+\.log$/', $filename)) {
$this->filesystem->remove($pathFile);
$this->addFlash('icon@success', 'Fichier supprimé avec succès');
} else {
$this->addFlash('icon@danger', 'Fichier introuvable');
}
return $this->redirectToRoute('dev.data.dbmanager');
}
/**
* Gestion des bases de données
*/
#[Route("/dbmanager/{filename}/import", name: "dev.data.dbmanager.import")]
public function import(string $filename): Response
{
$this->secureIfProd();
$filename = self::DUMP_PATH . $filename;
$env = $this->getParameter('kernel.environment');
if($env == 'prod'){
$this->addFlash('icon@danger', 'Import impossible en mode Production..');
return $this->redirectToRoute('dev.data.dbmanager');
}
if ($this->filesystem->exists($filename)) {
// création du fichier de lock pour les informations de dernier import
$this->filesystem->touch( $filename . '.lock');
$cfg = $this->getDoctrine()->getConnection('default')->getParams();
passthru('mysql -h '.$cfg['host'].' -u'.$cfg['user'].' -p'.$cfg['password'].' '.$cfg['dbname'].' < '.$filename);
$this->addFlash('icon@success', 'Le dump '. $filename .' a correctement été importé dans votre base de données');
return $this->redirectToRoute('dev.data.dbmanager');
}
$this->addFlash('icon@danger', 'Fichier introuvable');
return $this->redirectToRoute('dev.data.dbmanager');
}
/**
* Affiche les 40 derniéres notifications
*/
#[Route("/notifications", name: "dev.data.notifications")]
public function notifications(): Response
{
$this->secureIfProd();
return $this->render('dev/data/notifications.html.twig',[
'notifications' => $this->entityManager->getRepository(Notification::class)->findByNumber(200)
]);
}
/**
* Affiche les templates des emails envoyé (objet/body)
*/
#[Route("/notifications-templates", name: "dev.data.notifications.templates")]
public function showEmailsTemplate(): Response
{
$this->secureIfProd();
$templates = [];
$files = scandir('../templates/notification/email');
foreach($files as $file){
if($file[0] == '.' || $file == 'base.html.twig'){
continue;
}
$raw = file_get_contents('../templates/notification/email/'.$file);
$subject = 'unknown';
$body = 'unknown';
if(preg_match('#\{\% block subject \%\}(.*)\{\% endblock \%\}#isU', $raw, $match)){
$subject = $match[1];
}
if(preg_match('#\{\% block body \%\}(.*)\{\% endblock \%\}#isU', $raw, $match)){
$body = $match[1];
}
$templates[] = [
'file' => $file,
'subject' => $subject,
'body' => $body
];
}
return $this->render('dev/data/notifications-templates.html.twig',[
'templates' => $templates
]);
}
/**
* Envoie les emails de notification sans passer par la commande (pour les faignants !)
* alias de la commande `php bin/console app:send-mail-notifications`
*/
#[Route("/notifications-send", name: "dev.data.notifications.send")]
public function runSendNotification(KernelInterface $kernel): Response
{
$this->secureIfProd();
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'app:send-mail-notifications'
]);
$output = new BufferedOutput();
$application->run($input, $output);
$content = $output->fetch();
$this->addFlash('success', $content);
return $this->redirectToRoute('dev.data.notifications');
}
/**
* PoC du gestionnaire de notifications
*/
#[Route("/test-service-notifier", name: "dev.test.notifier")]
public function testServiceNotifier(Notifier $notifier, UserRepository $userRepository): RedirectResponse
{
$this->secureIfProd();
$user = $userRepository->findOneBy(['enabled' => 1]);
$notification = $notifier->createMail($user, 'resetting_password', [
'username' => $user->getNiceName(),
'confirmation_url' => 'https://www.google.fr'
]);
$this->addFlash('success', 'Nouvelle notification créee : #' . $notification->getId());
return $this->redirectToRoute('dev.data.notifications');
}
/**
* Envoi un email de test :
* http://symfony5.test/dev/test-mailer/mpommie@nowdigital.fr
*/
#[Route("/test-mailer/{email}", name: "dev.test.mailer")]
public function testMailer(string $email, Mailer $mailer): RedirectResponse
{
$this->secureIfProd();
try {
$state = $mailer->send(
$email,
'Test mailing plateforme SYMFONY5',
'<p><strong>Ceci est un test</strong> en <u>HTML</u></p>',
['debug' => true]
);
$this->addFlash($state ? 'success' : 'danger', 'Test mailing : ' . ($state ? 'Success' : 'Error'));
} catch (Exception $e) {
$this->addFlash('danger', $e->getMessage());
} catch (TransportExceptionInterface $e) {
$this->addFlash('danger', $e->getMessage());
}
return $this->redirectToRoute('dev.data.users');
}
/**
* Helper : Formate la taille des fichiers
*
* @param $bytes
* @param int $decimals
* @return string
*/
private function filesize($bytes, $decimals = 2) {
$sz = 'BKMGTP';
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor];
}
/**
* Voir configuration dans le .env
* - DEV_DATA_LOGIN
* - DEV_DATA_PASSWORD
*/
private function secureIfProd()
{
// Exception pour la plateforme de livraison Groupement, déjà équipée d'un htaccess mais en env=Prod
if($_SERVER['HTTP_HOST'] == 'symfony5-recette.nowdigital.fr'){
return true;
}
if(isset($_COOKIE['key']) && md5($_COOKIE['key']) == '5bc75bd65c17d97b41c50caeeadaa47a'){
return true;
}
$env = $this->getParameter('kernel.environment');
$login = $this->getParameter('dev_data_login');
$password = $this->getParameter('dev_data_password');
if ($env !== 'dev' && !preg_match('/(nowdigital.fr|\.test)$/i', $_SERVER['HTTP_HOST'])) {
if (!isset($_SERVER['PHP_AUTH_USER']) || $_SERVER['PHP_AUTH_USER'] != $login || !isset($_SERVER['PHP_AUTH_PW']) || $_SERVER['PHP_AUTH_PW'] != $password) {
header('WWW-Authenticate: Basic realm="HTACCESS"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
}
}
}