Procházet zdrojové kódy

test with extension - abandonned

Olivier Massot před 2 roky
rodič
revize
f9079e13c0

+ 0 - 7
config/packages/doctrine.yaml

@@ -30,13 +30,6 @@ doctrine:
         auto_generate_proxy_classes: true
         entity_managers:
             default:
-                filters:
-                    date_time_filter:
-                        class: App\Filter\DoctrineFilter\DateTimeFilter
-                        enabled: true
-                    activity_year_filter:
-                        class: App\Filter\DoctrineFilter\ActivityYearFilter
-                        enabled: true
                 connection: default
                 auto_mapping: true
                 mappings:

+ 0 - 3
config/services.yaml

@@ -124,9 +124,6 @@ services:
 
     #########################################
     ##  LISTENER ##
-    App\EventListener\DoctrineFilter\DoctrineFilterListener:
-        tags:
-            - { name: kernel.event_listener, event: kernel.request }
 
     #########################################
     ##  ELASTIC SERVICE ##

+ 1 - 1
sql/schema-extensions/002-view_federation_structures.sql

@@ -19,7 +19,7 @@ AS
              LEFT JOIN opentalent.AddressPostal a on oa.addressPostal_id = a.id
              LEFT JOIN opentalent.Country c ON (c.id = a.addressCountry_id)
              LEFT JOIN opentalent.organization_contactpoint ocp ON ocp.organization_id = o.id
-             LEFT JOIN (SELECT * FROM opentalent.ContactPoint WHERE `contactType`='PRINCIPAL') cp ON cp.id = ocp.contactPoint_id
+             INNER JOIN (SELECT * FROM opentalent.ContactPoint WHERE `contactType`='PRINCIPAL') cp ON cp.id = ocp.contactPoint_id
              LEFT JOIN (
                 SELECT oar_.organization_id, CONCAT('[', GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE('id', oar_.id, 'title', oar_.title, 'date', DATE_FORMAT(oar_.date, '%Y-%m-%dT%TZ'), 'link', oar_.link))), ']') as articles
                 FROM (SELECT * FROM OrganizationArticle WHERE link is not null and link != '' ORDER BY date DESC) as oar_

+ 114 - 0
src/Doctrine/TimeConstraint/AbstractTimeExtension.php

@@ -0,0 +1,114 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\TimeConstraint;
+
+use App\Attribute\ActivityYearConstraintAware;
+use App\Attribute\DateTimeConstraintAware;
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use App\Service\Utils\StringsUtils;
+use App\Tests\Filter\DoctrineFilter\DateTimeFilterTest;
+use Doctrine\Common\Collections\Criteria;
+use Doctrine\ORM\EntityManager;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Bundle\SecurityBundle\Security;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Applique les contraintes temporelles aux entités possédant l'annotation requise
+ */
+abstract class AbstractTimeExtension extends AbstractExtension
+{
+    protected ?Access $access = null;
+
+    protected static ?string $constraintAnnotation = null;
+
+    public function __construct(
+        protected EntityManagerInterface $entityManager,
+        protected Security $security,
+        protected RequestStack $requestStack
+    ) {
+        if (static::$constraintAnnotation === null) {
+            throw new \RuntimeException('Constraint annotation has not been set');
+        }
+    }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
+    {
+        $access = $this->security->getUser();
+        if (!$access) {
+            return;
+        }
+
+        if (!$this->areTimeConstraintsEnabled()) {
+            return;
+        }
+
+        $classMetadata = $this->entityManager->getClassMetadata($resourceClass);
+        $constraintAnnotation = $classMetadata->getReflectionClass()->getAttributes(static::$constraintAnnotation)[0] ?? null;
+        if (!$constraintAnnotation) {
+            return;
+        }
+
+        $startFieldName = $constraintAnnotation->getArguments()['startYearFieldName'] ?? null;
+        $endFieldName = $constraintAnnotation->getArguments()['endYearFieldName'] ?? null;
+        if (!$startFieldName || !$endFieldName) {
+            throw new \RuntimeException('Missing start and/or end field names in constraint annotation');
+        }
+
+        $constraints = $this->getConstraints($this->access->getId());
+
+        $fields = [
+            ActivityYearConstraint::START_KEY => $startFieldName,
+            ActivityYearConstraint::END_KEY => $endFieldName
+        ];
+
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+
+        foreach ($constraints as $key => $constraint) {
+            $orExpr = $queryBuilder->expr()->orX();
+
+            foreach ($constraint as $date => $conditions) {
+                foreach ($conditions as $condition) {
+                    $operator = $this->getSqlOperator($condition);
+
+                    if ($operator !== null) {
+                        $orExpr->add(sprintf("%s.%s %s '%s'", $rootAlias, $fields[$key], $operator, $date));
+                    } else {
+                        $orExpr->add(sprintf("%s.%s IS NULL", $rootAlias, $fields[$key]));
+                    }
+                }
+            }
+            $queryBuilder->andWhere($orExpr);
+        }
+    }
+
+    abstract protected function getConstraints(int $accessId): array;
+
+    protected function areTimeConstraintsEnabled(): bool {
+        return $this->requestStack->getMainRequest()->get('_time_constraint', true);
+    }
+
+    /**
+     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
+     * @param int $condition
+     * @return string|null
+     * @see DateTimeFilterTest::testGetArithmeticValue()
+     */
+    protected function getSqlOperator(int $condition): ?string
+    {
+        return match ($condition) {
+            DateTimeConstraint::INF => '<',
+            DateTimeConstraint::EQUAL => '=',
+            DateTimeConstraint::SUP => '>',
+            DateTimeConstraint::INF + DateTimeConstraint::EQUAL => '<=',
+            DateTimeConstraint::SUP + DateTimeConstraint::EQUAL => '>=',
+            default => null,
+        };
+    }
+}

+ 34 - 0
src/Doctrine/TimeConstraint/ActivityYearExtension.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Doctrine\TimeConstraint;
+
+use App\Attribute\ActivityYearConstraintAware;
+use App\Doctrine\AbstractExtension;
+use App\Service\Constraint\ActivityYearConstraint;
+use Doctrine\ORM\EntityManager;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Bundle\SecurityBundle\Security;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Applique les contraintes temporelles aux entités possédant l'annotation ActivityYearConstraintAware
+ */
+class ActivityYearExtension extends AbstractTimeExtension
+{
+    protected static ?string $constraintAnnotation = ActivityYearConstraintAware::class;
+
+    public function __construct(
+        protected EntityManagerInterface $entityManager,
+        protected Security $security,
+        protected RequestStack $requestStack,
+        protected ActivityYearConstraint $activityYearConstraint
+    )
+    {
+        parent::__construct($entityManager, $security, $requestStack);
+    }
+
+    protected function getConstraints(int $accessId): array
+    {
+        return $this->activityYearConstraint->invoke($accessId);
+    }
+}

+ 36 - 0
src/Doctrine/TimeConstraint/DateTimeExtension.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Doctrine\TimeConstraint;
+
+use App\Attribute\DateTimeConstraintAware;
+use App\Doctrine\AbstractExtension;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use Doctrine\ORM\EntityManager;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Bundle\SecurityBundle\Security;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Applique les contraintes temporelles aux entités possédant l'annotation DateTimeConstraintAware
+ */
+class DateTimeExtension extends AbstractTimeExtension
+{
+    protected static ?string $constraintAnnotation = DateTimeConstraintAware::class;
+
+    public function __construct(
+        protected EntityManagerInterface $entityManager,
+        protected Security $security,
+        protected RequestStack $requestStack,
+        protected DateTimeConstraint $dateTimeConstraint
+    )
+    {
+        parent::__construct($entityManager, $security, $requestStack);
+    }
+
+    protected function getConstraints(int $accessId): array
+    {
+        return $this->dateTimeConstraint->invoke($accessId);
+    }
+
+}

+ 0 - 0
src/Filter/Person/FullNameFilter.php → src/Filter/ApiPlatform/Person/FullNameFilter.php


+ 0 - 0
src/Filter/Utils/DistanceFilter.php → src/Filter/ApiPlatform/Utils/DistanceFilter.php


+ 0 - 0
src/Filter/Utils/FindInSetFilter.php → src/Filter/ApiPlatform/Utils/FindInSetFilter.php


+ 0 - 0
src/Filter/Utils/InFilter.php → src/Filter/ApiPlatform/Utils/InFilter.php


+ 135 - 0
src/Filter/Doctrine/TimeConstraint/AbstractTimeFilter.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+use App\Entity\Access\Access;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Query\Filter\SQLFilter;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Applique les contraintes temporelles aux entités possédant l'annotation requise
+ */
+abstract class AbstractTimeFilter extends SQLFilter
+{
+    protected ?Access $access = null;
+
+    /**
+     * Annotation expected, have to be re-defined in subclasses
+     * @var string|null
+     */
+    protected static ?string $constraintAnnotation = null;
+
+    // <--- Dependency injections
+    protected EntityManagerInterface $entityManager;
+    protected Security $security;
+    protected RequestStack $requestStack;
+
+    public function setEntityManagerInterface(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager;}
+    public function setSecurity(Security $security) { $this->security = $security;}
+    public function setRequestStack(RequestStack $requestStack) { $this->requestStack = $requestStack;}
+
+    // --->
+
+    /**
+     * Méthode surchargée de SQLFilter permettant d'appliquer un filtre supplémentaire aux requêtes SQL
+     *
+     * @param ClassMetadata $targetEntity
+     * @param string $targetTableAlias
+     * @return string
+     * @throws Exception
+     */
+    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
+    {
+        if (static::$constraintAnnotation === null) {
+            throw new \RuntimeException('Constraint annotation has not been set');
+        }
+
+        $access = $this->security->getUser();
+        if (!$access) {
+            return '';
+        }
+
+        if (!$this->areTimeConstraintsEnabled()) {
+            return '';
+        }
+
+        $constraintAnnotation = $targetEntity->getReflectionClass()->getAttributes(static::$constraintAnnotation)[0] ?? null;
+        if (!$constraintAnnotation) {
+            return '';
+        }
+
+        $startFieldName = $constraintAnnotation->getArguments()['startYearFieldName'] ?? null;
+        $endFieldName = $constraintAnnotation->getArguments()['endYearFieldName'] ?? null;
+        if (!$startFieldName || !$endFieldName) {
+            throw new \RuntimeException('Missing start and/or end field names in constraint annotation');
+        }
+
+        $constraints = $this->getConstraints($this->access->getId());
+
+        $fields = [
+            ActivityYearConstraint::START_KEY => $startFieldName,
+            ActivityYearConstraint::END_KEY => $endFieldName
+        ];
+
+        return $this->constructQuery($constraints, $targetTableAlias, $fields);
+    }
+
+    protected function areTimeConstraintsEnabled(): bool {
+        return $this->requestStack->getMainRequest()->get('_time_constraint', true);
+    }
+
+    abstract protected function getConstraints(int $accessId): array;
+
+    /**
+     * Fonction permettant de construire la requête SQL correspondante aux contraintes
+     *
+     * @param array<string, array<string, int[]>> $constraints
+     * @param string $targetTableAlias
+     * @param array<string, mixed> $fields
+     * @return string
+     */
+    protected function constructQuery(array $constraints, string $targetTableAlias, array $fields): string {
+        $queryConditionsAND = [];
+        foreach ($constraints as $key => $constraint) {
+            $queryConditionsOR = [];
+            foreach ($constraint as $date => $conditions){
+                foreach ($conditions as $condition){
+                    $arithmetic = $this->getArithmeticValue($condition);
+                    if(!is_null($arithmetic)) {
+                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
+                    }
+                    else {
+                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
+                    }
+                }
+            }
+            if(!empty($queryConditionsOR)) {
+                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
+            }
+        }
+
+        return implode(" AND ", $queryConditionsAND);
+    }
+
+    /**
+     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
+     * @param int $condition
+     * @return string|null
+     * @see DateTimeFilterTest::testGetArithmeticValue()
+     */
+    protected function getArithmeticValue(int $condition): ?string
+    {
+        return match ($condition) {
+            DateTimeConstraint::INF => '<',
+            DateTimeConstraint::EQUAL => '=',
+            DateTimeConstraint::SUP => '>',
+            DateTimeConstraint::INF + DateTimeConstraint::EQUAL => '<=',
+            DateTimeConstraint::SUP + DateTimeConstraint::EQUAL => '>=',
+            default => null,
+        };
+    }
+}

+ 32 - 0
src/Filter/Doctrine/TimeConstraint/ActivityYearFilter.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+use App\Attribute\ActivityYearConstraintAware;
+use App\Service\Constraint\ActivityYearConstraint;
+
+class ActivityYearFilter extends AbstractTimeFilter
+{
+    protected static ?string $constraintAnnotation = ActivityYearConstraintAware::class;
+
+    protected ActivityYearConstraint $activityYearConstraint;
+
+    /**
+     * Dependency injection
+     * @param ActivityYearConstraint $activityYearConstraint
+     * @return void
+     */
+    public function setActivityYearConstraint(ActivityYearConstraint $activityYearConstraint) {
+        $this->activityYearConstraint = $activityYearConstraint;
+    }
+
+    /**
+     * @param int $accessId
+     * @return array<string, array<string, list<int>>>
+     * @throws \Exception
+     */
+    protected function getConstraints(int $accessId): array
+    {
+        return $this->activityYearConstraint->invoke($accessId);
+    }
+}

+ 32 - 0
src/Filter/Doctrine/TimeConstraint/DatetimeExtensionFilter.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+use App\Attribute\DateTimeConstraintAware;
+use App\Service\Constraint\DateTimeConstraint;
+
+class DatetimeExtensionFilter extends AbstractTimeFilter
+{
+    protected static ?string $constraintAnnotation = DateTimeConstraintAware::class;
+
+    protected DateTimeConstraint $dateTimeConstraint;
+
+    /**
+     * Dependency injection
+     *
+     * @param DateTimeConstraint $dateTimeConstraint
+     * @return void
+     */
+    public function setDateTimeConstraint(DateTimeConstraint $dateTimeConstraint) {
+        $this->dateTimeConstraint = $dateTimeConstraint;
+    }
+
+    /**
+     * @param int $accessId
+     * @return array
+     */
+    protected function getConstraints(int $accessId): array
+    {
+        return $this->dateTimeConstraint->invoke($accessId);
+    }
+}

+ 0 - 64
src/Filter/DoctrineFilter/AbstractTimeFilter.php

@@ -1,64 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Filter\DoctrineFilter;
-
-use App\Attribute\DateTimeConstraintAware;
-use App\Service\Constraint\DateTimeConstraint;
-use App\Tests\Filter\DoctrineFilter\DateTimeFilterTest;
-use Doctrine\ORM\Query\Filter\SQLFilter;
-
-/**
- * Classe AbstractTimeFilterUtils fournie des méthodes pour construire les requetes de contrainte de temps
- */
-abstract class AbstractTimeFilter extends SQLFilter
-{
-    /**
-     * Fonction permettant de construire la requête SQL correspondante aux contraintes
-     * @param array<string, array<string, int[]>> $constraints
-     * @param string $targetTableAlias
-     * @param array<string, mixed> $fields
-     * @return string
-     */
-    protected function constructQuery(array $constraints, string $targetTableAlias, array $fields): string
-    {
-        $queryConditionsAND = [];
-        foreach ($constraints as $key => $constraint) {
-            $queryConditionsOR = [];
-            foreach ($constraint as $date => $conditions){
-                foreach ($conditions as $condition){
-                    $arithmetic = $this->getArithmeticValue($condition);
-                    if(!is_null($arithmetic)) {
-                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
-                    }
-                    else {
-                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
-                    }
-                }
-            }
-            if(!empty($queryConditionsOR)) {
-                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
-            }
-        }
-
-        return implode(" AND ", $queryConditionsAND);
-    }
-
-    /**
-     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
-     * @param int $condition
-     * @return string|null
-     * @see DateTimeFilterTest::testGetArithmeticValue()
-     */
-    protected function getArithmeticValue(int $condition): ?string
-    {
-        return match ($condition) {
-            DateTimeConstraint::INF => '<',
-            DateTimeConstraint::EQUAL => '=',
-            DateTimeConstraint::SUP => '>',
-            DateTimeConstraint::INF + DateTimeConstraint::EQUAL => '<=',
-            DateTimeConstraint::SUP + DateTimeConstraint::EQUAL => '>=',
-            default => null,
-        };
-    }
-}

+ 0 - 62
src/Filter/DoctrineFilter/ActivityYearFilter.php

@@ -1,62 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Filter\DoctrineFilter;
-
-use App\Attribute\ActivityYearConstraintAware;
-use App\Service\Constraint\ActivityYearConstraint;
-use App\Service\Utils\StringsUtils;
-use Doctrine\ORM\Mapping\ClassMetadata;
-use Exception;
-
-/**
- * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
- */
-final class ActivityYearFilter extends AbstractTimeFilter
-{
-    private ActivityYearConstraint $activityYearConstraint;
-
-    /**
-     * Méthode surchargée de SQLFilter
-     * @param ClassMetadata $targetEntity
-     * @param string $targetTableAlias
-     * @return string
-     * @throws Exception
-     */
-    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
-    {
-        if(!$this->hasParameter('_time_constraint')
-            || !(bool) StringsUtils::unquote($this->getParameter('_time_constraint'))
-            || !$this->hasParameter('accessId')
-        ) {
-            return '';
-        }
-
-        $yearConstraintAware = $targetEntity->getReflectionClass()->getAttributes(ActivityYearConstraintAware::class)[0] ?? null;
-        $startFieldName = $yearConstraintAware?->getArguments()['startYearFieldName'] ?? null;
-        $endFieldName = $yearConstraintAware?->getArguments()['endYearFieldName'] ?? null;
-        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
-            return '';
-        }
-
-        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
-
-        $constraints = $this->activityYearConstraint->invoke($accessId);
-
-        $fields = [
-            ActivityYearConstraint::START_KEY => $startFieldName,
-            ActivityYearConstraint::END_KEY => $endFieldName
-        ];
-
-        return $this->constructQuery($constraints, $targetTableAlias, $fields);
-    }
-
-    /**
-     * Permets d'assurer l'injection de dépendance du service YearTimeConstraint
-     * @param ActivityYearConstraint $activityYearConstraint
-     *
-     */
-    public function setActivityYearConstraint(ActivityYearConstraint $activityYearConstraint): void{
-        $this->activityYearConstraint = $activityYearConstraint;
-    }
-}

+ 0 - 58
src/Filter/DoctrineFilter/DateTimeFilter.php

@@ -1,58 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Filter\DoctrineFilter;
-
-use App\Attribute\DateTimeConstraintAware;
-use App\Service\Constraint\DateTimeConstraint;
-use App\Service\Utils\StringsUtils;
-use Doctrine\ORM\Mapping\ClassMetadata;
-
-/**
- * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
- */
-final class DateTimeFilter extends AbstractTimeFilter
-{
-    private DateTimeConstraint $dateTimeConstraint;
-
-    /**
-     * Méthode surchargée de SQLFilter
-     * @param ClassMetadata $targetEntity
-     * @param string $targetTableAlias
-     * @return string
-     * @throws \Exception
-     */
-    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
-    {
-        if(!$this->hasParameter('_time_constraint')
-            || !boolval(StringsUtils::unquote($this->getParameter('_time_constraint')))
-            || !$this->hasParameter('accessId')
-        )
-            return '';
-
-        $dateTimeConstraintAware = $targetEntity->getReflectionClass()->getAttributes(DateTimeConstraintAware::class)[0] ?? null;
-        $startFieldName = $dateTimeConstraintAware?->getArguments()['startDateFieldName'] ?? null;
-        $endFieldName = $dateTimeConstraintAware?->getArguments()['endDateFieldName'] ?? null;
-        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
-            return '';
-        }
-
-        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
-        $constraints = $this->dateTimeConstraint->invoke($accessId);
-
-        $fields = [
-            DateTimeConstraint::START_KEY => $startFieldName,
-            DateTimeConstraint::END_KEY => $endFieldName
-        ];
-
-        return $this->constructQuery($constraints, $targetTableAlias, $fields);
-    }
-
-    /**
-     * Permets d'assurer l'injection de dépendance du service DateTimeConstraint
-     * @param DateTimeConstraint $dateTimeConstraint
-     */
-    public function setDateTimeConstraint(DateTimeConstraint $dateTimeConstraint): void{
-        $this->dateTimeConstraint = $dateTimeConstraint;
-    }
-}

+ 195 - 0
src/Security/Voter/EntityVoter/AbstractEntityVoter.php

@@ -0,0 +1,195 @@
+<?php
+
+namespace App\Security\Voter\EntityVoter;
+
+use App\Entity\Access\Access;
+use App\Service\Access\Utils;
+use App\Service\Security\InternalRequestsService;
+use App\Service\Security\SwitchUser;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+
+/**
+ * Base class for custom Voters
+ *
+ * This class also defines a default behavior for entity based voters (ex: FileVoter)
+ *
+ * @see doc/security.md
+ */
+abstract class AbstractEntityVoter extends Voter
+{
+    protected const READ = 'READ';
+    protected const EDIT = 'EDIT';
+    protected const CREATE = 'CREATE';
+    protected const DELETE = 'DELETE';
+
+    /**
+     * The current user if any; access it trough isUserLoggedIn or getUser methods
+     * If the current user is null, it has not been fetched already
+     * If it is false, there is no user logged in
+     * @var Access|null|false
+     */
+    private Access|null|false $user = null;
+
+    /**
+     * The supported class name. Override it in subclass.
+     * Ex:
+     *     protected ?string $entityClass = File::class;
+     *
+     * @var string|null
+     */
+    protected static ?string $entityClass = null;
+
+    /**
+     * List of supported operations. Override it to restrict.
+     * @var array<string>
+     */
+    protected static array $allowedOperations = [
+        self::READ, self::EDIT, self::CREATE, self::DELETE
+    ];
+
+    public function __construct(
+        protected Security $security,
+        protected Utils $accessUtils,
+        private InternalRequestsService $internalRequestsService,
+        private EntityManagerInterface $em,
+        private SwitchUser $switchUser
+    ) {}
+
+    /**
+     * Default `supports` method, that uses self::entityClass and self::allowedOperations to determine if the voter
+     * supports the subject and attribute.
+     *
+     * @param string $attribute
+     * @param mixed $subject
+     * @return bool
+     */
+    protected function supports(string $attribute, mixed $subject): bool
+    {
+        if (static::$entityClass === null) {
+            throw new \RuntimeException('Setup the self::$entityClass property, or override the supports() method');
+        }
+
+        return $subject !== null && $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
+    }
+
+    /**
+     * Default `voteOnAttribute` method, calling one of the `canXxxx()` method, according to the attribute.
+     *
+     * @param string $attribute
+     * @param mixed $subject
+     * @param TokenInterface $token
+     * @return bool
+     */
+    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
+    {
+        switch ($attribute) {
+            case self::READ:
+                return $this->canView($subject);
+            case self::EDIT:
+                return $this->canEdit($subject);
+            case self::CREATE:
+                return $this->canCreate($subject);
+            case self::DELETE:
+                return $this->canDelete($subject);
+        }
+
+        return false;
+    }
+
+    /**
+     * Does the client have the right to view this resource?
+     * @param object $subject
+     * @return bool
+     */
+    protected function canView(object $subject): bool {
+        return false;
+    }
+
+    /**
+     * Does the client have the right to edit this resource?
+     * @param object $subject
+     * @return bool
+     */
+    protected function canEdit(object $subject): bool {
+        return false;
+    }
+
+    /**
+     * Does the client have the right to create this resource?
+     *
+     * @param object $subject
+     * @return bool
+     */
+    protected function canCreate(object $subject): bool {
+        return false;
+    }
+
+    /**
+     * Does the client have the right to delete this resource?
+     *
+     * @param object $subject
+     * @return bool
+     */
+    protected function canDelete(object $subject): bool {
+        return false;
+    }
+
+    /**
+     * Returns the current logged in user
+     * @return Access
+     */
+    protected function getUser(): ?Access {
+        if ($this->user === null) {
+            /** @var Access $user */
+            $user = $this->security->getUser();
+
+            // <-- Special case of impersonated users: the switch user is not setup yet by symfony, we have to do it "manually"
+            $switchHeaderId = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
+            if ($switchHeaderId !== null) {
+                $switchAs = $this->em->find(Access::class, $switchHeaderId);
+                if (
+                    $switchAs &&
+                    (
+                        $this->security->isGranted('ROLE_ALLOWED_TO_SWITCH') ||
+                        $this->switchUser->isAllowedToSwitch($user, $switchAs)
+                    )
+                ) {
+                    $user = $switchAs;
+                }
+            }
+            // -->
+
+            // If the user is not anonymous, remember it
+            $this->user = $user instanceof Access ? $user : false;
+        }
+
+        return $this->user !== false ? $this->user : null;
+    }
+
+    /**
+     * Is the client an authenticated user ?
+     *
+     * @return bool
+     */
+    protected function isUserLoggedIn(): bool {
+        return $this->getUser() !== null;
+    }
+
+    /**
+     * Is the current request a valid internal request?
+     *
+     * @see doc/internal_requests.md
+     * @see doc/security.md
+     *
+     * @return bool
+     */
+    protected function isValidInternalRequest(): bool {
+        $clientIp = $_SERVER['REMOTE_ADDR'] ?? null;
+        $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';
+
+        return $this->internalRequestsService->isAllowed($clientIp, $internalRequestToken);
+    }
+}

+ 3 - 3
src/Service/Constraint/ActivityYearConstraint.php

@@ -22,13 +22,13 @@ class ActivityYearConstraint extends AbstractTimeConstraintUtils
     /**
      * Main method
      *
-     * @param int $accessID
+     * @param int $accessId
      * @return array<string, array<string, list<int>>>
      * @throws \Exception
      */
-    public function invoke(int $accessID): array
+    public function invoke(int $accessId): array
     {
-        $access = $this->entityManager->getRepository(Access::class)->find($accessID);
+        $access = $this->entityManager->getRepository(Access::class)->find($accessId);
         $historical = $access->getHistorical();
 
         $contraints = [