<?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);
}
}