<?php
/** @noinspection ALL */
namespace App\Twig\Runtime;
use App\Constants\ACL;
use App\Entity\Parameter;
use App\Entity\SliderItem;
use App\Entity\User;
use App\Model\Product;
use App\Services\Back\ParameterService;
use App\Services\Common\AclServiceV2;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Twig\Extension\RuntimeExtensionInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
/**
* Rassemble toute les fonction disponible dans twig qui touchent aux ACL et à l'affichage des données via les règles acl ou les systèmes paralèlles (roles, jobs sur les entités comme pour les slider ou les documents)
*/
class AclRuntime implements RuntimeExtensionInterface
{
private AclServiceV2 $aclService;
private RequestStack $requestStack;
private ParameterService $parameterService;
private Security $security;
private RoleHierarchyInterface $roleHierarchy;
private EntityManagerInterface $em;
private array $userIsGrantedCache = [];
private array $componentVisibilityCache = [];
private array $displayConfigCache = [];
private ?string $currentRouteCache = null;
private mixed $currentRouteParamsCache = null;
private bool $currentRequestResolved = false;
private User|UserInterface|null $currentUserCache = null;
private bool $currentUserResolved = false;
public function __construct(
AclServiceV2 $aclService,
RequestStack $requestStack,
ParameterService $parameterService,
Security $security,
RoleHierarchyInterface $roleHierarchy,
EntityManagerInterface $em
)
{
$this->aclService = $aclService;
$this->requestStack = $requestStack;
$this->parameterService = $parameterService;
$this->security = $security;
$this->roleHierarchy = $roleHierarchy;
$this->em = $em;
}
/**
* Permet de savoir si le user a le droit d'accéder à la route
* @param User|null $user
* @param string $route
* @param array $params
* @param string $env
* @return bool
*/
public function userIsGrantedRoute(
?User $user,
string $route,
array $params = [],
string $env = ACL::FRONT_ENV,
bool $debug = FALSE
)
{
$config = [
'route' => $route,
'params' => $params,
'component' => ACL::ACL_NO_COMPONENT,
'slug' => ACL::ACL_NO_SLUG,
'action' => ACL::READ,
'env' => $env,
];
return $this->aclService->userIsGranted( $user, $config, $debug );
}
/**
* Permet de savoir si un user peut faire une action en fonction de son rôle ou de son job
*
* @param User|null $user
* @param string $slug
* @param string $action
* @param string $env
*
* @return bool
*
* @throws InvalidArgumentException
*/
public function userIsGranted(
?User $user,
string $component,
string $slug = ACL::ACL_NO_SLUG,
string $action = ACL::READ,
string $env = ACL::FRONT_ENV,
string $route = NULL,
string $params = NULL,
bool $debug = FALSE
): bool
{
[$resolvedRoute, $resolvedParams] = $this->getCurrentRouteContext();
$currentRoute = $route ?? $resolvedRoute;
$currentParams = $params ?? $resolvedParams;
if ($currentRoute === NULL){
return $this->checkWhenRouteIsNull();
}
$config = [
'route' => $currentRoute,
'params' => $this->aclService->getRouteParamsForAcl($currentRoute, $currentParams),
'component' => $component,
'slug' => $slug,
'action' => $action,
'env' => $env,
];
$cacheKey = implode('|', [
$user instanceof User ? $user->getId() : 'anonymous',
$currentRoute,
$config['params'],
$component,
$slug,
$action,
$env,
]);
if (array_key_exists($cacheKey, $this->userIsGrantedCache)) {
return $this->userIsGrantedCache[$cacheKey];
}
return $this->userIsGrantedCache[$cacheKey] = $this->aclService->userIsGranted( $user, $config, $debug );
}
/**
* Logique pour éviter des erreurs 500 si jamais une route est évaluée à NULL
* Si on est dans le cas d'une exception on autorise la visualition
* @return true
* @throws \Exception
*/
private function checkWhenRouteIsNull()
{
$request = $this->requestStack->getCurrentRequest();
$exeption = $request->attributes->get('exception');
if ($exeption !== null) {
return TRUE;
}
throw new \Exception('Une erreur est survenue, la route ne peut pas être null et ne pas être une exception');
}
/**
* Permet de savoir si le current user à le droit de voir le catalogue par son slug
* @param $catalogue
* @return bool
* @throws InvalidArgumentException
*/
public function userIsGrantedCatalogue($catalogue)
{
$currentUser = $this->security->getUser();
return $this->aclService->userIsGrantedCatalogue($currentUser, $catalogue);
}
/**
* Permet de savoir si le current user à le droit de voir le produit
* @param Product $product
*
* @return bool
* @throws \JsonException
*/
public function userIsGrantedProduct(Product $product)
{
$currentUser = $this->security->getUser();
return $this->aclService->userIsGrantedProduct($currentUser, $product);
}
/**
* Retourne le slug du premier catalogue qui contient le produit et qui est accèssible au user courant
* @param Product $product
*
* @return mixed|null
* @throws \JsonException
*/
public function getUserFirstGrantedCatalogSlugForProduct(Product $product)
{
$currentUser = $this->security->getUser();
return $this->aclService->getUserFirstGrantedCatalogSlugForProduct($currentUser, $product);
}
/**
* Défini si l'utilisateur à le droit de voir le document
*
* @param User|null $user
* @param Parameter $document
*
* @return bool
*
* @throws \JsonException
*/
public function canDisplayDocument( ?User $user, Parameter $document ): bool
{
return $this->aclService->userIsGrantedOnDocument( $user, $document );
}
public function filterVisibleDocuments(?User $user, array $documents, bool $onlyPublicOnSecurityRoute = false): array
{
[$currentRoute] = $this->getCurrentRouteContext();
$isSecurityRoute = $currentRoute !== null && in_array($currentRoute, ACL::ACL_SECURITY_ROUTES, true);
return array_values(array_filter($documents, function (Parameter $document) use ($user, $onlyPublicOnSecurityRoute, $isSecurityRoute) {
if (!$this->canDisplayDocument($user, $document)) {
return false;
}
if ($onlyPublicOnSecurityRoute && $isSecurityRoute && !$document->isPublic()) {
return false;
}
return true;
}));
}
/**
* Lors des documents personnalisé, permet de savoit si le user peut voir le document
*
* @param User|null $user
* @param $id
*
* @return bool
*
* @throws \JsonException
*/
public function isDocumentSelectedForUser( ?User $user, $id ): bool
{
return $this->parameterService->isDocumentSelectedForUser( $user, $id );
}
/**
* Permet de savoir si on component est visible par le user courant
*
* Ne doit être utilisé que pour l'affichage twig des components en front
*
* @param array|null $componentOptions
* @param bool $debug
* @return bool
* @throws InvalidArgumentException
*/
public function canDisplayComponentByAcl(?array $componentOptions, bool $debug = FALSE)
{
if ($componentOptions === NULL || $componentOptions === []){
return TRUE;
}
[$currentRoute] = $this->getCurrentRouteContext();
$currentUser = $this->getCurrentUser();
$item = $componentOptions['item'] ?? $componentOptions;
$display = $item['display'] ?? [];
$univers = $item['univers'] ?? [];
$componentAcl = $item['data']['data-component-acl'] ?? ACL::ACL_NO_COMPONENT;
$cacheKey = implode('|', [
$currentRoute ?? 'no-route',
$currentUser instanceof User ? $currentUser->getId() : 'anonymous',
$componentAcl,
(int)($item['enabled'] ?? true),
md5(json_encode($display)),
md5(json_encode($univers)),
]);
if (array_key_exists($cacheKey, $this->componentVisibilityCache)) {
return $this->componentVisibilityCache[$cacheKey];
}
// Le component est actif sur la page ?
$canDisplay = (bool)$item[ 'enabled' ] ?? TRUE;
if (!$canDisplay) {
return $this->componentVisibilityCache[$cacheKey] = FALSE;
}
// on regarde s'il peut s'afficher sur la page via la clef display
$canDisplay = $this->canDisplayOnPageByConfig($display, $debug);
if (!$canDisplay) {
return $this->componentVisibilityCache[$cacheKey] = FALSE;
}
// on recherche l'acl si on peut récupérer son slug data-component-acl
if (isset($item['data']['data-component-acl'])){
$canDisplay = $this->userIsGranted($currentUser instanceof User ? $currentUser : null, $componentAcl);
}
if (!$canDisplay) {
return $this->componentVisibilityCache[$cacheKey] = FALSE;
}
// on regarde s'il y a des univers... on verifie le currentUser car le component peut être utilisé en partie security
if ($currentUser instanceof User && $univers !== []){
if ( $canDisplay && !$currentUser->isDeveloper() && !$currentUser->isSuperAdmin()){
$canDisplay = $this->aclService->canDisplayByUniverses($currentUser, $univers);
}
}
return $this->componentVisibilityCache[$cacheKey] = $canDisplay;
}
/**
* Défini si un component s'affiche via la clef display
*
* Elle contient 2 enfants:
* enabled_on : array (tableau de route)|null
* disabled_on : array (tableau de route)|null
* Si disabled_on est a autre chose que null, alors c'est lui qui prend l'ascendant
* Afficher partout => enabled_on: null + disabled_on: [] ou null
*
* @param array $display
*
* @return bool
*/
private function canDisplayOnPageByConfig(array $display, bool $debug = FALSE)
{
[$currentRoute] = $this->getCurrentRouteContext();
if ($currentRoute === NULL){
return $this->checkWhenRouteIsNull();
}
$cacheKey = md5(json_encode([$currentRoute, $display]));
if (array_key_exists($cacheKey, $this->displayConfigCache)) {
return $this->displayConfigCache[$cacheKey];
}
$canDisplay = TRUE; // par défaut on affiche
// On prend la config du disabled_on si elle n'est pas nulle
$displayByDisabled = FALSE;
if (isset($display['disabled_on']) && $display['disabled_on'] !== NULL){
$displayByDisabled = TRUE;
}
// On prend la config enbaled_on
if (isset($display['enabled_on']) && !$displayByDisabled){
$arrayRoute = $display['enabled_on'];
if(gettype($display['enabled_on']) === "string") {
$arrayRoute = json_decode($display['enabled_on'], true);
}
switch (TRUE){
// tableau vide, ce n'est pas visible
case $arrayRoute === []:
$canDisplay = FALSE;
break;
// NULL ou valeur qui n'est pas un tableau, on considère que c'est visible
// ainsi si enabled_on et disabled_on sont NULL, on affiche
case $arrayRoute === NULL:
case !is_array($arrayRoute):
$canDisplay = TRUE;
break;
// on regarde si la route actuelle est dans le tableau pour l'afficher
default:
$canDisplay = in_array( $currentRoute, $arrayRoute, TRUE );
break;
}
}
// on prend la config disabled_on
if (isset($display['disabled_on']) && $displayByDisabled){
$arrayRoute = $display['disabled_on'];
if(gettype($display['disabled_on']) === "string") {
$arrayRoute = json_decode($display['disabled_on'], true);
}
switch (TRUE){
// tableau vide, c'est visible partout
case $arrayRoute === []:
$canDisplay = TRUE;
break;
// NULL ou valeur qui n'est pas un tableau, on refuse l'affichage
// Normalement on ne tombe pas dans cette configuration puisque $displayByDisabled est FALSE
case $arrayRoute === NULL:
case !is_array($arrayRoute):
$canDisplay = FALSE;
break;
// on regarde si la route actuelle est dans le tableau pour refuser l'affichage
default:
$canDisplay = !in_array( $currentRoute, $arrayRoute, TRUE );
break;
}
}
return $this->displayConfigCache[$cacheKey] = $canDisplay;
}
private function getCurrentRouteContext(): array
{
if ($this->currentRequestResolved) {
return [$this->currentRouteCache, $this->currentRouteParamsCache];
}
$request = $this->requestStack->getCurrentRequest();
$this->currentRouteCache = $request?->get('_route');
$this->currentRouteParamsCache = $request?->get('_route_params');
$this->currentRequestResolved = true;
return [$this->currentRouteCache, $this->currentRouteParamsCache];
}
private function getCurrentUser(): User|UserInterface|null
{
if ($this->currentUserResolved) {
return $this->currentUserCache;
}
$this->currentUserCache = $this->security->getUser();
$this->currentUserResolved = true;
return $this->currentUserCache;
}
/**
* Permet de savoir si le user peut voir le slider
*
* Les acl du slider sont intégré à l'édition de l'entité
*
* @param User $user
* @param SliderItem $item
*
* @return bool
*/
public function canDisplaySliderItem(?User $user, SliderItem $item): bool
{
if(!$user) return FALSE;
$jobs = $item->getDisplayJob();
$universes = $item->getDisplayUniverses();
// Le superadmin et dev doivent pouvoir tout voir...
if ( $user->isDeveloper() || $user->isSuperAdmin() ) {
return TRUE;
}
// Par défaut, tout s'affiche
$canDisplay = TRUE;
// SI config par job on regarde si ça match
if ( $jobs !== NULL && !in_array( $user->getJob(), $jobs, TRUE ) ) {
$canDisplay = FALSE;
}
// Si config par univers on regarde si ça match
if ( $universes !== NULL ) {
foreach ( $user->getUniverses() as $userUnivers ) {
if ( in_array( $userUnivers->getSlug(), $universes, TRUE ) ) {
$canDisplay = TRUE;
break;
}
$canDisplay = FALSE;
}
}
return $canDisplay;
}
/**
* Normalise la transformation de l'array qui contient les params d'une route pour la transformer en string
* @param array $params
*
* @return false|string
* @throws \JsonException
*/
public function formatParamsToString(array $params)
{
return $this->aclService->formatArrayParamsToString($params);
}
/**
* Retourne un tableau avec la liste de roles et les jobs relatif à ces roles
*
* @return array[]
*/
public function getDefaultRolesAndJobs()
{
return $this->aclService->getDefaultRoleAndJob();
}
/**
* @return UserInterface|null
*/
public function getOriginalUser(): ?UserInterface
{
$token = $this->security->getToken();
if ($token instanceof SwitchUserToken)
{
$user = $token->getOriginalToken()->getUser();
$user = $this->em->getRepository(User::class)->findOneBy(['email' => $user->getEmail()]);
return $user;
}
return $this->security->getUser();
}
/**
* @param User $user
* @param string $role
*
* @return bool
*/
public function hasRole(User $user, string $role): bool
{
$reachableRoles = $this->roleHierarchy->getReachableRoleNames($user->getRoles());
return in_array($role, $reachableRoles);
}
}