src/Controller/DevController.php line 92

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Service\Mailer;
  4. use App\Service\Notifier;
  5. use App\Entity\Notification;
  6. use App\Entity\User;
  7. use App\Repository\UserRepository;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use Exception;
  10. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  11. use Symfony\Bundle\FrameworkBundle\Console\Application;
  12. use Symfony\Component\Console\Input\ArrayInput;
  13. use Symfony\Component\Console\Output\BufferedOutput;
  14. use Symfony\Component\Filesystem\Filesystem;
  15. use Symfony\Component\Finder\Finder;
  16. use Symfony\Component\Form\Extension\Core\Type\TextType;
  17. use Symfony\Component\HttpFoundation\RedirectResponse;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\HttpKernel\KernelInterface;
  21. use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
  22. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  23. use Symfony\Component\Routing\Annotation\Route;
  24. use Symfony\Contracts\Service\Attribute\Required;
  25. #[Route("/dev")]
  26. class DevController extends AbstractController
  27. {
  28.     const DUMP_PATH '../var/dump/';
  29.     #[Required]
  30.     public KernelInterface $kernel;
  31.     #[Required]
  32.     public Filesystem $filesystem;
  33.     #[Required]
  34.     public EntityManagerInterface $entityManager;
  35.     /**
  36.      * Contrôles :
  37.      *  - Intégrité de la base de donnée : projets/porteurs orphelins ?
  38.      *  - Post import : projets/porteurs incomplet ?
  39.      *  - Chmod des dossiers principaux..
  40.      */
  41.     #[Route("/checkup"name"dev.data.checkup")]
  42.     public function controls()
  43.     {
  44.         $this->secureIfProd();
  45.         // Remonté des config php
  46.         $phps = [
  47.             'memory_limit'          => false,
  48.             'upload_max_filesize'   => false,
  49.             'post_max_size'         => false,
  50.             'max_execution_time'    => false,
  51.             'max_input_time'        => false,
  52.             'disable_functions'     => false
  53.         ];
  54.         foreach($phps as $php => $state){
  55.             $phps[$php] = ini_get($php);
  56.         }
  57.         // Check des droits d'écriture
  58.         $dirs = [
  59.             'var'           => false,
  60.             'var/export'    => false,
  61.             'var/yarn'      => false,
  62.             'var/log'       => false,
  63.             'var/dump'      => false,
  64.             'var/cache'     => false,
  65.             'var/upload'    => false,
  66.             'public/upload' => false,
  67.         ];
  68.         foreach($dirs as $dir => $state){
  69.             $dirs[$dir] = is_dir('../'.$dir) && is_writable('../'.$dir);
  70.         }
  71.         return $this->render('dev/data/controls.html.twig', [
  72.             'infoPhp' => $phps,
  73.             'infoChmod' => $dirs,
  74.         ]);
  75.     }
  76.     /**
  77.      * Listing complet des utilisateurs
  78.      */
  79.     #[Route("/data"name"dev.data.users")]
  80.     public function users(EntityManagerInterface $em)
  81.     {
  82.         $this->secureIfProd();
  83.         /** @var UserRepository $userRepository */
  84.         $userRepository $em->getRepository(User::class);
  85.         $roles $this->getParameter('security.role_hierarchy.roles');
  86.         $users = [];
  87.         foreach($roles as $role) {
  88.             $role $role[0];
  89.             if ($role === 'ROLE_USER') {
  90.                 continue;
  91.             }
  92.             $users[$role] = $userRepository->findAllByRolesOrderByStructure([$role]);
  93.         }
  94.         // Limiter à 1000 utilisateurs par rôle
  95.         $rolesTruncated = [];       // Afficher un message au niveau des rôles correspondants
  96.         $maxUsersDisplayed 1000;  // Nombre maximum
  97.         foreach($users as $role => $usersHavingRole) {
  98.             if (count($usersHavingRole) > $maxUsersDisplayed) {
  99.                 $users[$role] = array_slice($usersHavingRole0$maxUsersDisplayed);
  100.                 $rolesTruncated []= $role;
  101.             }
  102.         }
  103.         $git = [
  104.             'time' => file_exists('../.git/index') ? filectime('../.git/index') : 0,
  105.             'hash' => file_exists('../.git/ORIG_HEAD') ? file_get_contents('../.git/ORIG_HEAD') : '',
  106.         ];
  107.         return $this->render('dev/data/users.html.twig', [
  108.             'usersByRole' => $users,
  109.             'infoGit' => $git,
  110.             'rolesTruncated' => $rolesTruncated,
  111.             'maxUsersDisplayed' => $maxUsersDisplayed,
  112.         ]);
  113.     }
  114.     /**
  115.      * Permet de se connecter "en tant que"..
  116.      */
  117.     #[Route("/login/{id}"name"dev.data.login")]
  118.     public function login($idEntityManagerInterface $em)
  119.     {
  120.         $this->secureIfProd();
  121.         $user $em->getRepository(User::class)->find($id);
  122.         if ($user) {
  123.             /** @var User $user */
  124.             $providerKey 'main'// La clé du firewall
  125.             $token = new UsernamePasswordToken($user$user->getPassword(), $providerKey$user->getRoles());
  126.             $this->get('security.token_storage')->setToken($token);
  127.             $this->get('session')->set('_security_' $providerKeyserialize($token));
  128.             // N'existe plus ?
  129.             // $event = new InteractiveLoginEvent($request, $token);
  130.             // $dispatcher->dispatch('security.interactive_login', $event);
  131.             $this->addFlash('success','Désormais connecté en tant que "' $user->getEmail() . '", rôle "' implode(','$user->getRoles()) . '"');
  132.             return $this->redirectToRoute('home');
  133.         }
  134.         $this->addFlash('warning''Utilisateur introuvable..');
  135.         return $this->redirectToRoute('dev.data.users');
  136.     }
  137.     /**
  138.      * Gestion des bases de données
  139.      */
  140.     #[Route("/dbmanager"name"dev.data.dbmanager")]
  141.     public function dbmanager(): Response
  142.     {
  143.         $this->secureIfProd();
  144.         $dumps = new Finder();
  145.         $dumps->sortByModifiedTime();
  146.         $dumps->reverseSorting();
  147.         if(!file_exists(self::DUMP_PATH)) {
  148.             mkdir(self::DUMP_PATH);
  149.         }
  150.         $files = [];
  151.         foreach ($dumps->files()->in(self::DUMP_PATH) as $dump) {
  152.             if(preg_match('/.lock$/'$dump->getFilename())) {
  153.                 continue;
  154.             }
  155.             if ($this->filesystem->exists(self::DUMP_PATH $dump->getFilename() . '.lock')) {
  156.                 $import = new Finder();
  157.                 $import->name($dump->getFilename() . '.lock');
  158.                 foreach ($import->in(self::DUMP_PATH) as $file) {
  159.                     $dump->import = (New \DateTime())->setTimestamp($file->getATime())->format('d/m/Y \\à H:i');
  160.                 }
  161.             } else {
  162.                 $dump->import 'jamais';
  163.             }
  164.             $dump->filesize $this->filesize($dump->getSize());
  165.             array_push($files$dump);
  166.         }
  167.         $logs = [];
  168.         $dumps = new Finder();
  169.         $dumps->sortByModifiedTime();
  170.         $dumps->reverseSorting();
  171.         foreach ($dumps->files()->in('../var/log') as $log) {
  172.             $log->filesize $this->filesize($log->getSize());
  173.             array_push($logs$log);
  174.         }
  175.         return $this->render('dev/data/dbmanager.html.twig',[
  176.             'dumps' => $files,
  177.             'logs' => $logs,
  178.             'dbinfo' => $this->getDoctrine()->getConnection('default')->getParams()
  179.         ]);
  180.     }
  181.     /**
  182.      * Créer un dump
  183.      */
  184.     #[Route("/dbmanager/dump"name"dev.data.dbmanager.dump")]
  185.     public function dump(Request $request): Response
  186.     {
  187.         $this->secureIfProd();
  188.         if ($request->getMethod() === 'GET') {
  189.             $form $this->createFormBuilder()
  190.                 ->add('name'TextType::class, [
  191.                     'label' => 'Nom du dump',
  192.                     'required' => true
  193.                 ])
  194.                 ->getForm();
  195.             if ($request->isXmlHttpRequest()) {
  196.                 return $this->render('dev/data/partials/_create_dump.html.twig', [
  197.                     'form' => $form->createView()
  198.                 ]);
  199.             }
  200.         }
  201.         if ($request->getMethod() === 'POST') {
  202.             $form $this->createFormBuilder()
  203.                 ->setMethod('POST')
  204.                 ->add('name'TextType::class, [
  205.                     'label' => 'Nom du dump',
  206.                     'required' => true
  207.                 ])
  208.                 ->getForm();
  209.             $form->handleRequest($request);
  210.             if ($form->isSubmitted() && $form->isValid()) {
  211.                 $name strtolower(str_replace(' ''_'$request->request->get('form')['name']));
  212.                 $filename self::DUMP_PATH $name '.sql';
  213.                 if ($this->filesystem->exists($filename)) {
  214.                     $this->addFlash('icon@danger''Le nom de fichier existe déjà');
  215.                     return $this->redirectToRoute('dev.data.dbmanager');
  216.                 }
  217.                 $cfg $this->getDoctrine()->getConnection('default')->getParams();
  218.                 try {
  219.                     passthru('mysqldump -h '.$cfg['host'].' -u'.$cfg['user'].' -p'.$cfg['password'].' '.$cfg['dbname'].' > '.$filename);
  220.                     if ($this->filesystem->exists($filename)) {
  221.                         $this->addFlash('icon@success''Dump effectué avec succès.');
  222.                     }
  223.                 } catch (Exception $exception){
  224.                     $this->addFlash('icon@danger'"Une erreur s'est produite.. : ".$exception->getMessage());
  225.                 }
  226.             }
  227.         }
  228.         return $this->redirectToRoute('dev.data.dbmanager');
  229.     }
  230.     /**
  231.      * Gestion des bases de données
  232.      */
  233.     #[Route("/dbmanager/download/{filename}"name"dev.data.dbmanager.download")]
  234.     public function downloadDB(string $filename): Response
  235.     {
  236.         $this->secureIfProd();
  237.         $filename self::DUMP_PATH $filename;
  238.         if ($this->filesystem->exists($filename)) {
  239.             return $this->file($filename);
  240.         }
  241.         $this->addFlash('icon@danger''Fichier introuvable');
  242.         return $this->redirectToRoute('dev.data.dbmanager');
  243.     }
  244.     /**
  245.      * Téléchargement d'un fichier de log
  246.      */
  247.     #[Route("/dbmanager/log/download/{filename}"name"dev.data.log.download")]
  248.     public function downloadLogFile(string $filename): Response
  249.     {
  250.         $this->secureIfProd();
  251.         $pathFile '../var/log/' $filename;
  252.         if ($this->filesystem->exists($pathFile) && preg_match('/^[0-9a-z\-\_]+\.log$/'$filename)) {
  253.             return $this->file($pathFile);
  254.         }
  255.         $this->addFlash('icon@danger''Fichier introuvable');
  256.         return $this->redirectToRoute('dev.data.dbmanager');
  257.     }
  258.     /**
  259.      * Suppression d'un fichier de log
  260.      */
  261.     #[Route("/dbmanager/log/remove/{filename}"name"dev.data.log.remove")]
  262.     public function removeLogFile(string $filename): Response
  263.     {
  264.         $this->secureIfProd();
  265.         $pathFile '../var/log/' $filename;
  266.         if ($this->filesystem->exists($pathFile) && preg_match('/^[0-9a-z\-\_]+\.log$/'$filename)) {
  267.             $this->filesystem->remove($pathFile);
  268.             $this->addFlash('icon@success''Fichier supprimé avec succès');
  269.         } else {
  270.             $this->addFlash('icon@danger''Fichier introuvable');
  271.         }
  272.         return $this->redirectToRoute('dev.data.dbmanager');
  273.     }
  274.     /**
  275.      * Gestion des bases de données
  276.      */
  277.     #[Route("/dbmanager/{filename}/import"name"dev.data.dbmanager.import")]
  278.     public function import(string $filename): Response
  279.     {
  280.         $this->secureIfProd();
  281.         $filename self::DUMP_PATH $filename;
  282.         $env $this->getParameter('kernel.environment');
  283.         if($env == 'prod'){
  284.             $this->addFlash('icon@danger''Import impossible en mode Production..');
  285.             return $this->redirectToRoute('dev.data.dbmanager');
  286.         }
  287.         if ($this->filesystem->exists($filename)) {
  288.             // création du fichier de lock pour les informations de dernier import
  289.             $this->filesystem->touch$filename '.lock');
  290.             $cfg $this->getDoctrine()->getConnection('default')->getParams();
  291.             passthru('mysql -h '.$cfg['host'].' -u'.$cfg['user'].' -p'.$cfg['password'].' '.$cfg['dbname'].' < '.$filename);
  292.             $this->addFlash('icon@success''Le dump '$filename .' a correctement été importé dans votre base de données');
  293.             return $this->redirectToRoute('dev.data.dbmanager');
  294.         }
  295.         $this->addFlash('icon@danger''Fichier introuvable');
  296.         return $this->redirectToRoute('dev.data.dbmanager');
  297.     }
  298.     /**
  299.      * Affiche les 40 derniéres notifications
  300.      */
  301.     #[Route("/notifications"name"dev.data.notifications")]
  302.     public function notifications(): Response
  303.     {
  304.         $this->secureIfProd();
  305.         return $this->render('dev/data/notifications.html.twig',[
  306.             'notifications' => $this->entityManager->getRepository(Notification::class)->findByNumber(200)
  307.         ]);
  308.     }
  309.     /**
  310.      * Affiche les templates des emails envoyé (objet/body)
  311.      */
  312.     #[Route("/notifications-templates"name"dev.data.notifications.templates")]
  313.     public function showEmailsTemplate(): Response
  314.     {
  315.         $this->secureIfProd();
  316.         $templates = [];
  317.         $files scandir('../templates/notification/email');
  318.         foreach($files as $file){
  319.             if($file[0] == '.' || $file == 'base.html.twig'){
  320.                 continue;
  321.             }
  322.             $raw file_get_contents('../templates/notification/email/'.$file);
  323.             $subject 'unknown';
  324.             $body    'unknown';
  325.             if(preg_match('#\{\% block subject \%\}(.*)\{\% endblock \%\}#isU'$raw$match)){
  326.                 $subject $match[1];
  327.             }
  328.             if(preg_match('#\{\% block body \%\}(.*)\{\% endblock \%\}#isU'$raw$match)){
  329.                 $body $match[1];
  330.             }
  331.             $templates[] = [
  332.                 'file' => $file,
  333.                 'subject' => $subject,
  334.                 'body' => $body
  335.             ];
  336.         }
  337.         return $this->render('dev/data/notifications-templates.html.twig',[
  338.             'templates' => $templates
  339.         ]);
  340.     }
  341.     /**
  342.      * Envoie les emails de notification sans passer par la commande (pour les faignants !)
  343.      * alias de la commande `php bin/console app:send-mail-notifications`
  344.      */
  345.     #[Route("/notifications-send"name"dev.data.notifications.send")]
  346.     public function runSendNotification(KernelInterface $kernel): Response
  347.     {
  348.         $this->secureIfProd();
  349.         $application = new Application($kernel);
  350.         $application->setAutoExit(false);
  351.         $input = new ArrayInput([
  352.             'command' => 'app:send-mail-notifications'
  353.         ]);
  354.         $output = new BufferedOutput();
  355.         $application->run($input$output);
  356.         $content $output->fetch();
  357.         $this->addFlash('success'$content);
  358.         return $this->redirectToRoute('dev.data.notifications');
  359.     }
  360.     /**
  361.      * PoC du gestionnaire de notifications
  362.      */
  363.     #[Route("/test-service-notifier"name"dev.test.notifier")]
  364.     public function testServiceNotifier(Notifier $notifierUserRepository $userRepository): RedirectResponse
  365.     {
  366.         $this->secureIfProd();
  367.         $user $userRepository->findOneBy(['enabled' => 1]);
  368.         $notification $notifier->createMail($user'resetting_password', [
  369.             'username' => $user->getNiceName(),
  370.             'confirmation_url' => 'https://www.google.fr'
  371.         ]);
  372.         $this->addFlash('success''Nouvelle notification créee : #' $notification->getId());
  373.         return $this->redirectToRoute('dev.data.notifications');
  374.     }
  375.     /**
  376.      * Envoi un email de test :
  377.      * http://symfony5.test/dev/test-mailer/mpommie@nowdigital.fr
  378.      */
  379.     #[Route("/test-mailer/{email}"name"dev.test.mailer")]
  380.     public function testMailer(string $emailMailer $mailer): RedirectResponse
  381.     {
  382.         $this->secureIfProd();
  383.         try {
  384.             $state $mailer->send(
  385.                 $email,
  386.                 'Test mailing plateforme SYMFONY5',
  387.                 '<p><strong>Ceci est un test</strong> en <u>HTML</u></p>',
  388.                 ['debug' => true]
  389.             );
  390.             $this->addFlash($state 'success' 'danger''Test mailing : ' . ($state 'Success' 'Error'));
  391.         } catch (Exception $e) {
  392.             $this->addFlash('danger'$e->getMessage());
  393.         } catch (TransportExceptionInterface $e) {
  394.             $this->addFlash('danger'$e->getMessage());
  395.         }
  396.         return $this->redirectToRoute('dev.data.users');
  397.     }
  398.     /**
  399.      * Helper : Formate la taille des fichiers
  400.      *
  401.      * @param $bytes
  402.      * @param int $decimals
  403.      * @return string
  404.      */
  405.     private function filesize($bytes$decimals 2) {
  406.         $sz 'BKMGTP';
  407.         $factor floor((strlen($bytes) - 1) / 3);
  408.         return sprintf("%.{$decimals}f"$bytes pow(1024$factor)) . @$sz[$factor];
  409.     }
  410.     /**
  411.      * Voir configuration dans le .env
  412.      * - DEV_DATA_LOGIN
  413.      * - DEV_DATA_PASSWORD
  414.      */
  415.     private function secureIfProd()
  416.     {
  417.         // Exception pour la plateforme de livraison Groupement, déjà équipée d'un htaccess mais en env=Prod
  418.         if($_SERVER['HTTP_HOST'] == 'symfony5-recette.nowdigital.fr'){
  419.             return true;
  420.         }
  421.         if(isset($_COOKIE['key']) && md5($_COOKIE['key']) == '5bc75bd65c17d97b41c50caeeadaa47a'){
  422.             return true;
  423.         }
  424.         $env $this->getParameter('kernel.environment');
  425.         $login $this->getParameter('dev_data_login');
  426.         $password $this->getParameter('dev_data_password');
  427.         if ($env !== 'dev' && !preg_match('/(nowdigital.fr|\.test)$/i'$_SERVER['HTTP_HOST'])) {
  428.             if (!isset($_SERVER['PHP_AUTH_USER']) || $_SERVER['PHP_AUTH_USER'] != $login || !isset($_SERVER['PHP_AUTH_PW']) || $_SERVER['PHP_AUTH_PW'] != $password) {
  429.                 header('WWW-Authenticate: Basic realm="HTACCESS"');
  430.                 header('HTTP/1.0 401 Unauthorized');
  431.                 exit;
  432.             }
  433.         }
  434.     }
  435. }