<?php
namespace App\Service\Commun;
use App\Entity\AssistantMaternel\AssistantMaternel;
use App\Entity\Commun\Utilisateur;
use App\Entity\Commun\UtilisateurPreferenceNotif;
use App\Entity\Parametrage\EnumPreferenceNotifs;
use App\Entity\Parametrage\EnumTypeNotification;
use App\Entity\Parametrage\EnumProfil;
use App\Entity\Parametrage\Profil;
use App\Entity\Referentiel\SecteurPmi;
use App\Service\AssistantMaternel\AssistantMaternelService;
use App\Service\Parametrage\ProfilService;
use App\Service\Commun\TraceurService;
use App\Service\Notification\NotificationService;
use App\Entity\Commun\Traceur;
use App\Utils\TraceurUtils;
use DateInterval;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Psr\Log\LoggerInterface;
class UtilisateurService
{
// pour avoir accès à la BDD (doctrine)
private $em;
//log
private $logger;
// validation des entités
private $validator;
private $passwordEncoder;
private $profilService;
private $assmatService;
private $mailService;
private $traceurService;
private $notificationService;
public function __construct(
EntityManagerInterface $em,
ValidatorInterface $validator,
UserPasswordEncoderInterface $passwordEncoder,
ProfilService $profilService,
AssistantMaternelService $assmatService,
TokenStorageInterface $tokenStorage,
MailService $mailService,
TraceurService $traceurService,
NotificationService $notificationService,
LoggerInterface $logger
) {
// injection de dépendance pour avoir accès à la BDD (doctrine)
$this->em = $em;
$this->validator = $validator;
$this->passwordEncoder = $passwordEncoder;
$this->profilService = $profilService;
$this->assmatService = $assmatService;
$this->tokenStorage = $tokenStorage; //TODO : à supprimer si on ne gare pas la fonctionnalit de rester connecté après changement de mail
$this->mailService = $mailService;
$this->traceurService = $traceurService;
$this->notificationService = $notificationService;
$this->logger = $logger;
}
/**
* Vérifie si l'email fourni existe déjà en base en tant que login d'une assmat ou d'un utilisateur
*
* @param string $email
* @param Utilisateur $user
* @return string vide si l'email n'esiste pas, le message d'erreur sinon
*/
public function checkEmailUnique($email, Utilisateur $user)
{
$userMemeMail = $this->em->getRepository(Utilisateur::class)->findBy(array('email' => $email));
if ($userMemeMail != null) {
foreach ($userMemeMail as $value) {
if ($value->getId() != $user->getId()) {
return "Cet email est déjà utilisé par un autre utilisateur.";
}
}
}
$assmatMemeMail = $this->em->getRepository(AssistantMaternel::class)->findBy(array('email' => $email));
if ($assmatMemeMail != null) {
foreach ($assmatMemeMail as $value) {
if ($value->getUtilisateur()->getId() != $user->getId()) {
return "Cet email est déjà utilisé par un autre assistant maternel.";
}
}
}
return '';
}
/**
* Enregistre les modifications de caractériques d'une assmat (en tant qu'utilisateur)
*
* @param Utilisateur $user
* @param Utilisateur $currentUser
* @param Request $request
* @return void
*/
public function modifierUtilisateurAssmat(Utilisateur $user, Utilisateur $currentUser, Request $request)
{
// Récupérer les paramètres de la requête
$email = $request->get('email');
$actif = $request->get('actif');
$archive = $request->get('archive');
// Mapping sur l'utilisateur
// L'email est stocké au niveau de l'assmat
if (!is_null($actif)) {
$user->setActif($actif);
}
// TODO déplacer dans service Assmat ?
$assmat = $user->getAssistantMaternel();
if (!is_null($archive)) {
$assmat->setEstArchiveDomicile($archive);
$assmat->setEstArchiveMam($archive);
}
if (!is_null($email)) {
// Validation spécifique ici car pas de validation au niveau de l'entité (email nullable)
if (empty($email)) {
// Erreur
return "Vous devez saisir une adresse email.";
}
$oldEmail = $assmat->getEmail();
$assmat->setEmail($email);
}
// Validation (pas de formulaire)
$errorsU = $this->validator->validate($user);
if (count($errorsU) > 0) { //NOSONAR empty n'est pas équivalent à count > 0 ici
// TODO throw exception de validation + catch + print controller ?
// Par simplicité on affiche seulement la 1ère erreur
return $errorsU->get(0)->getMessage();
}
// TODO déplacer dans service Assmat ?
$errorsAM = $this->validator->validate($assmat);
if (count($errorsAM) > 0) { //NOSONAR empty n'est pas équivalent à count > 0 ici
// TODO throw exception de validation + catch + print controller ?
// Par simplicité on affiche seulement la 1ère erreur
return $errorsAM->get(0)->getMessage();
}
//verification unicite email
if ($email != null) {
$checkUnique = $this->checkEmailUnique($email, $user);
if ($checkUnique !== '') {
return $checkUnique;
}
}
// Enregistrement
$laTraceUser = $this->tracerUtilisateur($currentUser, $user);
$laTraceAssmat = $this->assmatService->tracerAssmat($currentUser, $assmat);
$this->enregistrerUtilisateur($user, $currentUser);
$this->assmatService->enregistrerAssmatSimple($assmat, $currentUser, false);
if (!is_null($email)) {
$this->notificationService->ajouterOuMettreAJourNotification($assmat->getId(), EnumTypeNotification::CHANGEMENT_EMAIL, '', is_null($oldEmail) ? '' : $oldEmail);
}
// maj statut depassement
$this->assmatService->computeDepassement($assmat, false); // (des)archivage d'une assmat
// ecrire les traces
$this->traceurService->ecrireTrace($laTraceUser, null);
$this->traceurService->ecrireTrace($laTraceAssmat, null);
// Succès
return true;
}
/**
*
* @param Utilisateur $user
* @param Request $request
* @return boolean|array
* @throws Exception
*/
public function modifierUtilisateurDepartement(Utilisateur $user, Utilisateur $currentUser, Request $request)
{
// Récupérer les paramètres de la requête
// Null si non défini
$email = $request->get('email');
$idProfil = $request->get('profil');
$idSecteurPmi = $request->get('secteurPmi');
$supprime = $request->get('supprime');
$actif = $request->get('actif');
$idAncienProfil = $user->getProfil()->getId();
// Mapping sur l'utilisateur
if (!is_null($email)) {
// Validation spécifique ici car pas de validation au niveau de l'entité (email nullable)
if (empty($email)) {
// Erreur
return "Vous devez saisir une adresse email.";
}
$user->setEmail($email);
}
if (!is_null($idProfil)) {
$profil = $this->em->find(Profil::class, $idProfil);
$user->setProfil($profil);
}
if (!is_null($idSecteurPmi)) {
if ($idSecteurPmi) {
// Secteur de PMI (non vide) sélectionné
$secteurPmi = $this->em->find(SecteurPmi::class, $idSecteurPmi);
} else {
// Pas de secteur de PMI sélectionné ($idSecteurPmi="")
$secteurPmi = null;
}
$user->setSecteurPmi($secteurPmi);
}
if (!is_null($actif)) {
$user->setActif($actif);
}
// null est évalué à false
if ($supprime) {
// on ne peut pas désupprimer un utilisateur
$user->setSupprime(true);
}
// Validation à la main car pas de formulaire
$errors = $this->validator->validate($user);
if (count($errors) > 0) { //NOSONAR empty n'est pas équivalent à count > 0 ici
// TODO throw exception de validation + catch + print controller ?
// Par simplicité on affiche seulement la 1ère erreur
return $errors->get(0)->getMessage();
}
//verification unicite email
//unicite entre utilisateurs déjà géré par doctrine (contrainte d'unicité)
// TODO checkEmailUnique ?
$assmatMemeMail = $this->em->getRepository(AssistantMaternel::class)->findBy(array('email' => $user->getEmail()));
if ($assmatMemeMail != null) {
return "Cet email est déjà utilisé par un autre assistant maternel.";
}
// initialise les préférences de notification si devient puer, les supprime si était puer
$this->gererPreferencesNotification($user, $idAncienProfil, $idProfil);
// Enregistrement
$laTraceUser = $this->tracerUtilisateur($currentUser, $user);
$this->enregistrerUtilisateur($user, $currentUser);
$this->traceurService->ecrireTrace($laTraceUser, null);
$this->em->flush();
// Succès
return true;
}
/**
* Permet à l'utilisateur de rester connecté après avoir changé de login
* TODO : setToken(new UsernamePasswordToken) ne fonctionne plus aprsè gestion obso
*
* @param type $newUser
*/
public function rafraichirTokenUtilisateur($newUser)
{
// Récupérer le token actuel pour les données qui ne changent pas
$oldToken = $this->tokenStorage->getToken();
// Créer un nouveau token avec l'utilisateur mis à jour
$token = new UsernamePasswordToken(
$newUser, // user object with updated username
null,
$oldToken->getProviderKey(),
$oldToken->getRoles()
);
// Mettre à jour le token
$this->tokenStorage->setToken($token);
}
public function enregistrerMotDePasse(Utilisateur $user, string $password)
{
// Encoder le mot de passe
$password_hash = $this->passwordEncoder->encodePassword($user, $password);
$user->setPassword($password_hash);
// Invalider le code d'activation ou de réinitialisation
$user->setCodeActivation(null);
// Enregistrement
$this->em->persist($user);
$this->em->flush();
}
public function creerUtilisateurAssmat($assmat)
{
// création de l'utilisateur
$utilisateur = new Utilisateur();
$utilisateur->setProfil($this->profilService->getProfil(EnumProfil::ASSMAT));
$utilisateur->setAssistantMaternel($assmat);
// Les champs de l'objet Assmat ont déjà été validés par l'orchestrateur
$utilisateur->setNom($assmat->getNomFamille());
$utilisateur->setPrenom($assmat->getPrenom());
// TODO factoriser valeurs par défaut
$utilisateur->setActif(true);
$utilisateur->setSupprime(false);
// L'assmat n'a pas de mot de passe tant que son compte n'est pas activé
$utilisateur->setPassword('');
return $utilisateur;
}
/**
* Créer les comptes utilisateurs des assmats qui n'ont pas encore de compte
*
* @param int $limiteComptesAssmats Nombre de comptes maximum à créer
*/
public function creerComptesUtilisateursAssmats($limiteComptesAssmats = null)
{
$batchSize = 500;
// TODO centralisation log
echo "[DEBUG] Récupération des assmats sans compte..." . PHP_EOL;
// Structure de données itérable renvoyée par DOctrine qui permet de
// ne pas charger tous les objets en mémoire d'un seul coup
$iterableResult = $this->em->getRepository(AssistantMaternel::class)->findSansCompteUtilisateur($limiteComptesAssmats);
$nb_assmat = 0;
echo "[DEBUG] Memory usage before: " . (memory_get_usage() / 1024) . " KB" . PHP_EOL;
$s = microtime(true);
foreach ($iterableResult as $row) {
$assmat = $row[0];
$nb_assmat++;
// créer utilisateur pour l'assmat $assmat
$utilisateur = $this->creerUtilisateurAssmat($assmat);
// association
$assmat->setUtilisateur($utilisateur);
// enregistrement en base
$this->em->persist($utilisateur);
$this->em->persist($assmat);
// Traitement par batchs pour libérer de la mémoire au fil de l'exécution
if ($nb_assmat % $batchSize === 0) {
$this->em->flush();
$this->em->clear();
echo "[DEBUG] Memory usage: " . (memory_get_usage() / 1024) . " KB" . PHP_EOL;
echo "[DEBUG] $nb_assmat assmats traités" . PHP_EOL;
}
// On limite le nombre d'insertions pour les jeux de tests de dev
if (!is_null($limiteComptesAssmats) && $nb_assmat >= $limiteComptesAssmats) {
break;
}
}
// flush final
$this->em->flush();
// fin
echo "[DEBUG] Memory usage after: " . (memory_get_usage() / 1024) . " KB" . PHP_EOL;
$e = microtime(true);
echo "[DEBUG] Inserted $nb_assmat objects in " . ($e - $s) . ' seconds' . PHP_EOL;
}
/**
* Génération du code d'activation de compte
* Le code est alphanumerique sur 15 caractères.
* Le code est valide 7 jours.
* Le code remplace tout code préalablement existant.
*
* @param Utilisateur $user utilisateur concerné par la demande de reinit
* @return string
*/
private function genererCodeActivation(Utilisateur $user)
{
// Générer un nouveau code d'activation
// On utilise un multiple de 3 car base64_encode encode 3 bytes en 4 caractères
// (évite d'avoir des caractères "=" en fin de chaîne)
// Avec 15 bytes on génère un code de 20 caractères
// On supprime les caractères spéciaux pour que le code passe dans l'URL
$regex = "/[^a-zA-Z0-9]/";
$code_activation = preg_replace($regex, '', base64_encode(random_bytes(15)));
// Mettre une date limite de validité au token
$date_validite = new \DateTime();
$interval = DateInterval::createfromdatestring('+7 day');
$date_validite->add($interval);
// Enregistrer le token dans l'utilisateur
// Si un code existait déjà, le nouveau code annule et remplace l'ancien
$user->setCodeActivation($code_activation);
$user->setDateLimiteCodeActivation($date_validite);
$this->em->persist($user);
$this->em->flush();
return $code_activation;
}
/**
* Génère un code d'activation pour l'utilisateur et
* envoie l'email correspondant à l'activation du compte
*
* @param Utilisateur $user utilisateur concerné par la demande de reinit
*/
public function envoyerCodeActivation(Utilisateur $user)
{
// Générer le code
$code = $this->genererCodeActivation($user);
// Envoyer le code à l'utilisateur par mail
try {
return $this->mailService->envoyerMailActivation($user, $code);
} catch (\Error $err) {
$this->logger->critical('ECHEC envoyerMailActivation : ' . $err->getMessage());
$this->logger->critical($err);
}
}
/**
* Génère un code d'activation pour l'utilisateur et
* envoie l'email correspondant à la réinitialisation de mot de passe
*
* @param Utilisateur $user utilisateur concerné par la demande de reinit
*/
public function demanderReinitialisationMotDePasse(Utilisateur $user)
{
if (!$user->getActif() || $user->getSupprime()) {
// Ne rien faire
$this->logger->info("Demande de réinitialisation de mot de passe ignorée : utilisateur désactivé ou supprimé", $user);
return null;
}
// Générer un nouveau code
$code = $this->genererCodeActivation($user);
// Envoyer le code à l'utilisateur par mail
return $this->mailService->envoyerMailReinitialisation($user, $code);
}
/**
* vérifie si le code d'activation est valide par raport à l'email
*
* @param string $code
* @param string $email
* @return bool
*/
public function verifierCodeActivation(string $code, string $email)
{
$repo = $this->em->getRepository(Utilisateur::class);
// Réutilisation de la fonction utilisée par l'authentification
// qui permet de retrouver un utilisateur par email utilisateur ou email assmat
$user = $repo->loadUserByUsername($email);
if (!$user) {
// Aucun utilisateur n'existe avec cette adresse
return false;
}
if ($user->getCodeActivation() !== $code) {
// Le code d'activation associé à cet utilisateur n'existe pas
// (mauvais code, ou le code a été remplacé par un nouveau)
return false;
}
$now = new \DateTime();
if ($user->getDateLimiteCodeActivation() < $now || !$user->getActif() || $user->getSupprime()) {
// Code périmé
return false;
}
// Le code d'activation/réinitialisation existe, est bien associé à l'email, et n'est pas périmé
return true;
}
/**
* Enregistrement de l'utilisateur
*
* @param Utilisateur $user utilisateur en cours d'édition
* @param Utilisateur $currentUser auteur de l'édition
* @param Traceur $laTrace
* @return Utilisateur
*/
public function enregistrerUtilisateur(Utilisateur $user, Utilisateur $currentUser, Traceur $laTrace = null)
{
try {
if ($laTrace == null) {
$laTrace = $this->tracerUtilisateur($currentUser, $user);
}
$this->em->persist($user);
} catch (\Error $err) {
$this->traceurService->ecrireEchec($laTrace, $err->getTraceAsString());
throw $err;
} catch (\Exception $ex) {
$this->traceurService->ecrireEchec($laTrace, $ex->getTraceAsString());
throw $ex;
}
return $user;
}
/**
* Trace les modifications effectuées sur l'utlisateur
*
* @param Utilisateur $currentUser auteur de la modification
* @param Utilisateur $user en cours d'édition (ajout ou modification)
*/
public function tracerUtilisateur(Utilisateur $currentUser, Utilisateur $user)
{
try {
$json = '';
if ($user->getId() > 0) {
//delta de modification
$originalData = $this->em->getUnitOfWork()->getOriginalEntityData($user);
$originalEntityArray = Utilisateur::createFromArray($originalData)->jsonSerialize();
$toArrayEntity = $user->jsonSerialize();
$changes = TraceurUtils::array_diff_assoc_recursive($toArrayEntity, $originalEntityArray);
$json = json_encode($changes);
} else {
//création
$json = json_encode($user->jsonSerialize());
}
return $this->traceurService->genereTrace(
$currentUser,
null,
$user->getId(),
"utilisateur",
"modification utilisateur [" . $user->getIdFonctionnel() . " - " . strtoupper($user->getNom()) . " " . $user->getPrenom() . " - " . $user->getEmail() . "][" . $user->getProfil()->getLibelle() . "]",
$json
);
} catch (\Exception $e) {
$this->logger->critical('Exception reçue : ' . $e->getMessage());
$this->logger->critical($e);
}
}
/**
* Gère le changemnt de profil et son impact sur les préférences interveant
* si $user devient puer, activer les notifs
* si était puer et ne l'est plus, supprimer les préférences de notif
* @param Utilisateur $user
* @param type $idAncienProfil
* @param type $idProfil
* @return Utilisateur
*/
public function gererPreferencesNotification(Utilisateur $user, $idAncienProfil, $idProfil)
{
if ($idAncienProfil == $idProfil) {
return $user;
}
if ($idProfil == EnumProfil::PUER) {
//initialiser les préférences
foreach (EnumTypeNotification::getListeTypeNotification() as $idTypeNotif) {
$initPref = new UtilisateurPreferenceNotif();
$initPref->setUtilisateur($user);
$initPref->setTypeNotification($idTypeNotif);
$initPref->setPreferenceNotification(EnumPreferenceNotifs::QUODITIENNE);
$this->em->persist($initPref);
$this->em->flush();
}
}
if ($idAncienProfil == EnumProfil::PUER) {
//supprimer les préférences
foreach ($user->getPreferencesNotification() as $pref) {
$this->em->remove($pref);
$this->em->flush();
}
}
return $user;
}
}