From 112488d3b3efa7e2704f33e0d62adf09e6b147b1 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 14 May 2024 20:44:57 -0500 Subject: [PATCH 1/2] Internal: Add course access tracking- BT#21694 --- .../EventListener/CidReqListener.php | 50 ++++++++++++ .../EventListener/CourseAccessListener.php | 81 ++++++++++++++----- src/CoreBundle/Resources/config/listeners.yml | 9 +-- 3 files changed, 114 insertions(+), 26 deletions(-) diff --git a/src/CoreBundle/EventListener/CidReqListener.php b/src/CoreBundle/EventListener/CidReqListener.php index 1ae000a4772..bc378351dc8 100644 --- a/src/CoreBundle/EventListener/CidReqListener.php +++ b/src/CoreBundle/EventListener/CidReqListener.php @@ -9,6 +9,7 @@ use Chamilo\CoreBundle\Controller\EditorController; use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Session; +use Chamilo\CoreBundle\Entity\TrackECourseAccess; use Chamilo\CoreBundle\Entity\User; use Chamilo\CoreBundle\Exception\NotAllowedException; use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter; @@ -17,6 +18,7 @@ use Chamilo\CourseBundle\Controller\CourseControllerInterface; use Chamilo\CourseBundle\Entity\CGroup; use ChamiloSession; +use DateTime; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; @@ -268,6 +270,13 @@ public function cleanSessionHandler(Request $request): void ChamiloSession::erase('course_already_visited'); } + $user = $this->tokenStorage->getToken()->getUser(); + $courseId = $sessionHandler->get('cid', 0); + $sessionId = $sessionHandler->get('sid', 0); + $ip = $request->getClientIp(); + if ($courseId !== 0) { + $this->logoutAccess($user, $courseId, $sessionId, $ip); + } $sessionHandler->remove('toolgroup'); $sessionHandler->remove('_cid'); $sessionHandler->remove('cid'); @@ -326,4 +335,45 @@ private function generateCourseUrl(?Course $course, int $sessionId, int $groupId return ''; } + + private function logoutAccess(User $user, int $courseId, int $sessionId, string $ip): void + { + $now = new DateTime("now", new \DateTimeZone("UTC")); + $sessionLifetime = 3600; + $limitTime = (new DateTime())->setTimestamp(time() - $sessionLifetime); + + $access = $this->entityManager->getRepository(TrackECourseAccess::class) + ->createQueryBuilder('a') + ->where('a.user = :user AND a.cId = :courseId AND a.sessionId = :sessionId') + ->andWhere('a.loginCourseDate > :limitTime') + ->setParameters([ + 'user' => $user, + 'courseId' => $courseId, + 'sessionId' => $sessionId, + 'limitTime' => $limitTime, + ]) + ->orderBy('a.loginCourseDate', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + + if ($access) { + $access->setLogoutCourseDate($now); + $access->setCounter($access->getCounter() + 1); + $this->entityManager->flush(); + } else { + // No access found or existing access is outside the session lifetime + // Insert new access record + $newAccess = new TrackECourseAccess(); + $newAccess->setUser($user); + $newAccess->setCId($courseId); + $newAccess->setSessionId($sessionId); + $newAccess->setUserIp($ip); + $newAccess->setLoginCourseDate($now); + $newAccess->setLogoutCourseDate($now); + $newAccess->setCounter(1); + $this->entityManager->persist($newAccess); + $this->entityManager->flush(); + } + } } diff --git a/src/CoreBundle/EventListener/CourseAccessListener.php b/src/CoreBundle/EventListener/CourseAccessListener.php index 6091639e209..9eaa331d654 100644 --- a/src/CoreBundle/EventListener/CourseAccessListener.php +++ b/src/CoreBundle/EventListener/CourseAccessListener.php @@ -7,10 +7,13 @@ namespace Chamilo\CoreBundle\EventListener; use Chamilo\CoreBundle\Entity\TrackECourseAccess; -use Chamilo\CourseBundle\Event\CourseAccess; -use Doctrine\ORM\EntityManager; -use Symfony\Component\HttpFoundation\Request; +use Chamilo\CoreBundle\Entity\User; +use Chamilo\CoreBundle\ServiceHelper\CidReqHelper; +use DateTime; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; /** * In and outs of a course @@ -18,30 +21,66 @@ */ class CourseAccessListener { - protected ?Request $request = null; - public function __construct( - private readonly EntityManager $em, - RequestStack $requestStack - ) { - $this->request = $requestStack->getCurrentRequest(); + private readonly EntityManagerInterface $em, + private readonly RequestStack $requestStack, + private readonly CidReqHelper $cidReqHelper, + private readonly TokenStorageInterface $tokenStorage, + ) {} + + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest() || $event->getRequest()->attributes->get('access_checked')) { + // If it's not the main request or we've already handled access in this request, do nothing + return; + } + + $courseId = (int) $this->cidReqHelper->getCourseId(); + $sessionId = (int) $this->cidReqHelper->getSessionId(); + + if ($courseId > 0) { + $user = $this->tokenStorage->getToken()?->getUser(); + if ($user instanceof User) { + $ip = $this->requestStack->getCurrentRequest()->getClientIp(); + $access = $this->findExistingAccess($user, $courseId, $sessionId); + + if ($access) { + $this->updateAccess($access); + } else { + $this->recordAccess($user, $courseId, $sessionId, $ip); + } + + // Set a flag on the request to indicate that access has been checked + $event->getRequest()->attributes->set('access_checked', true); + } + } } - public function __invoke(CourseAccess $event): void + private function findExistingAccess(User $user, int $courseId, int $sessionId) { - // CourseAccess - $user = $event->getUser(); - $course = $event->getCourse(); - $ip = $this->request->getClientIp(); + return $this->em->getRepository(TrackECourseAccess::class) + ->findOneBy(['user' => $user, 'cId' => $courseId, 'sessionId' => $sessionId]); + } - $access = new TrackECourseAccess(); - $access - ->setCId($course->getId()) - ->setUser($user) - ->setSessionId(0) - ->setUserIp($ip) - ; + private function updateAccess(TrackECourseAccess $access): void + { + $now = new DateTime(); + if (!$access->getLogoutCourseDate() || $now->getTimestamp() - $access->getLogoutCourseDate()->getTimestamp() > 300) { + $access->setLogoutCourseDate($now); + $access->setCounter($access->getCounter() + 1); + $this->em->flush(); + } + } + private function recordAccess(User $user, int $courseId, int $sessionId, string $ip): void + { + $access = new TrackECourseAccess(); + $access->setUser($user); + $access->setCId($courseId); + $access->setSessionId($sessionId); + $access->setUserIp($ip); + $access->setLoginCourseDate(new \DateTime()); + $access->setCounter(1); $this->em->persist($access); $this->em->flush(); } diff --git a/src/CoreBundle/Resources/config/listeners.yml b/src/CoreBundle/Resources/config/listeners.yml index c3b234ae970..5ace2e1c315 100644 --- a/src/CoreBundle/Resources/config/listeners.yml +++ b/src/CoreBundle/Resources/config/listeners.yml @@ -17,12 +17,11 @@ services: # Sets the user access in a course listener Chamilo\CoreBundle\EventListener\CourseAccessListener: - arguments: - - '@doctrine.orm.entity_manager' - tags: - - {name: kernel.event_listener, event: chamilo_course.course.access} + tags: + - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 6 } + - # Sets the user access in a course session listener + # Sets the user access in a course session listener Chamilo\CoreBundle\EventListener\SessionAccessListener: tags: - {name: kernel.event_listener, event: chamilo_course.course.session} From e550fc59cf60e705abe3cc17bea3f2f47e68ec9d Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Thu, 16 May 2024 00:20:31 -0500 Subject: [PATCH 2/2] Internal: Refactor course access tracking- BT#21694 --- public/main/inc/lib/tracking.lib.php | 2 +- public/main/my_space/myStudents.php | 2 +- .../EventListener/CidReqListener.php | 54 +++---------- .../EventListener/CourseAccessListener.php | 46 +++-------- .../TrackECourseAccessRepository.php | 81 +++++++++++++++++++ src/CoreBundle/Resources/config/listeners.yml | 2 +- 6 files changed, 103 insertions(+), 84 deletions(-) diff --git a/public/main/inc/lib/tracking.lib.php b/public/main/inc/lib/tracking.lib.php index 9a6445cf24e..2fae9bbc3e2 100644 --- a/public/main/inc/lib/tracking.lib.php +++ b/public/main/inc/lib/tracking.lib.php @@ -5712,7 +5712,7 @@ public static function show_course_detail($userId, $courseId, $sessionId = 0, $s ); $last_connection_in_lp = self::get_last_connection_time_in_lp( $userId, - $course, + $course->getCode(), $lp_id, $sessionId ); diff --git a/public/main/my_space/myStudents.php b/public/main/my_space/myStudents.php index 1e99cc3bb34..3ff488debe7 100644 --- a/public/main/my_space/myStudents.php +++ b/public/main/my_space/myStudents.php @@ -1705,7 +1705,7 @@ // Get last connection time in lp $start_time = Tracking::get_last_connection_time_in_lp( $studentId, - $course, + $course->getCode(), $lp_id, $sessionId ); diff --git a/src/CoreBundle/EventListener/CidReqListener.php b/src/CoreBundle/EventListener/CidReqListener.php index bc378351dc8..fbe96b549ff 100644 --- a/src/CoreBundle/EventListener/CidReqListener.php +++ b/src/CoreBundle/EventListener/CidReqListener.php @@ -41,7 +41,7 @@ public function __construct( private readonly AuthorizationCheckerInterface $authorizationChecker, private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $entityManager, - private readonly TokenStorageInterface $tokenStorage, + private readonly TokenStorageInterface $tokenStorage ) {} /** @@ -270,12 +270,19 @@ public function cleanSessionHandler(Request $request): void ChamiloSession::erase('course_already_visited'); } - $user = $this->tokenStorage->getToken()->getUser(); $courseId = $sessionHandler->get('cid', 0); $sessionId = $sessionHandler->get('sid', 0); $ip = $request->getClientIp(); if ($courseId !== 0) { - $this->logoutAccess($user, $courseId, $sessionId, $ip); + $token = $this->tokenStorage->getToken(); + if (null !== $token) { + /** @var User $user */ + $user = $token->getUser(); + if ($user instanceof UserInterface) { + $this->entityManager->getRepository(TrackECourseAccess::class) + ->logoutAccess($user, $courseId, $sessionId, $ip); + } + } } $sessionHandler->remove('toolgroup'); $sessionHandler->remove('_cid'); @@ -335,45 +342,4 @@ private function generateCourseUrl(?Course $course, int $sessionId, int $groupId return ''; } - - private function logoutAccess(User $user, int $courseId, int $sessionId, string $ip): void - { - $now = new DateTime("now", new \DateTimeZone("UTC")); - $sessionLifetime = 3600; - $limitTime = (new DateTime())->setTimestamp(time() - $sessionLifetime); - - $access = $this->entityManager->getRepository(TrackECourseAccess::class) - ->createQueryBuilder('a') - ->where('a.user = :user AND a.cId = :courseId AND a.sessionId = :sessionId') - ->andWhere('a.loginCourseDate > :limitTime') - ->setParameters([ - 'user' => $user, - 'courseId' => $courseId, - 'sessionId' => $sessionId, - 'limitTime' => $limitTime, - ]) - ->orderBy('a.loginCourseDate', 'DESC') - ->setMaxResults(1) - ->getQuery() - ->getOneOrNullResult(); - - if ($access) { - $access->setLogoutCourseDate($now); - $access->setCounter($access->getCounter() + 1); - $this->entityManager->flush(); - } else { - // No access found or existing access is outside the session lifetime - // Insert new access record - $newAccess = new TrackECourseAccess(); - $newAccess->setUser($user); - $newAccess->setCId($courseId); - $newAccess->setSessionId($sessionId); - $newAccess->setUserIp($ip); - $newAccess->setLoginCourseDate($now); - $newAccess->setLogoutCourseDate($now); - $newAccess->setCounter(1); - $this->entityManager->persist($newAccess); - $this->entityManager->flush(); - } - } } diff --git a/src/CoreBundle/EventListener/CourseAccessListener.php b/src/CoreBundle/EventListener/CourseAccessListener.php index 9eaa331d654..364b155344c 100644 --- a/src/CoreBundle/EventListener/CourseAccessListener.php +++ b/src/CoreBundle/EventListener/CourseAccessListener.php @@ -9,6 +9,7 @@ use Chamilo\CoreBundle\Entity\TrackECourseAccess; use Chamilo\CoreBundle\Entity\User; use Chamilo\CoreBundle\ServiceHelper\CidReqHelper; +use Chamilo\CoreBundle\ServiceHelper\UserHelper; use DateTime; use Symfony\Component\HttpKernel\Event\RequestEvent; use Doctrine\ORM\EntityManagerInterface; @@ -23,9 +24,8 @@ class CourseAccessListener { public function __construct( private readonly EntityManagerInterface $em, - private readonly RequestStack $requestStack, private readonly CidReqHelper $cidReqHelper, - private readonly TokenStorageInterface $tokenStorage, + private readonly UserHelper $userHelper ) {} public function onKernelRequest(RequestEvent $event): void @@ -39,15 +39,16 @@ public function onKernelRequest(RequestEvent $event): void $sessionId = (int) $this->cidReqHelper->getSessionId(); if ($courseId > 0) { - $user = $this->tokenStorage->getToken()?->getUser(); - if ($user instanceof User) { - $ip = $this->requestStack->getCurrentRequest()->getClientIp(); - $access = $this->findExistingAccess($user, $courseId, $sessionId); + $user = $this->userHelper->getCurrent(); + if ($user) { + $ip = $event->getRequest()->getClientIp(); + $accessRepository = $this->em->getRepository(TrackECourseAccess::class); + $access = $accessRepository->findExistingAccess($user, $courseId, $sessionId); if ($access) { - $this->updateAccess($access); + $accessRepository->updateAccess($access); } else { - $this->recordAccess($user, $courseId, $sessionId, $ip); + $accessRepository->recordAccess($user, $courseId, $sessionId, $ip); } // Set a flag on the request to indicate that access has been checked @@ -55,33 +56,4 @@ public function onKernelRequest(RequestEvent $event): void } } } - - private function findExistingAccess(User $user, int $courseId, int $sessionId) - { - return $this->em->getRepository(TrackECourseAccess::class) - ->findOneBy(['user' => $user, 'cId' => $courseId, 'sessionId' => $sessionId]); - } - - private function updateAccess(TrackECourseAccess $access): void - { - $now = new DateTime(); - if (!$access->getLogoutCourseDate() || $now->getTimestamp() - $access->getLogoutCourseDate()->getTimestamp() > 300) { - $access->setLogoutCourseDate($now); - $access->setCounter($access->getCounter() + 1); - $this->em->flush(); - } - } - - private function recordAccess(User $user, int $courseId, int $sessionId, string $ip): void - { - $access = new TrackECourseAccess(); - $access->setUser($user); - $access->setCId($courseId); - $access->setSessionId($sessionId); - $access->setUserIp($ip); - $access->setLoginCourseDate(new \DateTime()); - $access->setCounter(1); - $this->em->persist($access); - $this->em->flush(); - } } diff --git a/src/CoreBundle/Repository/TrackECourseAccessRepository.php b/src/CoreBundle/Repository/TrackECourseAccessRepository.php index 9c1b9bbcf61..746ffca5b66 100644 --- a/src/CoreBundle/Repository/TrackECourseAccessRepository.php +++ b/src/CoreBundle/Repository/TrackECourseAccessRepository.php @@ -8,6 +8,7 @@ use Chamilo\CoreBundle\Entity\TrackECourseAccess; use Chamilo\CoreBundle\Entity\User; +use DateTime; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -46,4 +47,84 @@ public function getLastAccessByUser(?User $user = null): ?TrackECourseAccess return null; } + + /** + * Find existing access for a user. + */ + public function findExistingAccess(User $user, int $courseId, int $sessionId) + { + return $this->findOneBy(['user' => $user, 'cId' => $courseId, 'sessionId' => $sessionId]); + } + + /** + * Update access record. + */ + public function updateAccess(TrackECourseAccess $access): void + { + $now = new DateTime(); + if (!$access->getLogoutCourseDate() || $now->getTimestamp() - $access->getLogoutCourseDate()->getTimestamp() > 300) { + $access->setLogoutCourseDate($now); + $access->setCounter($access->getCounter() + 1); + $this->_em->flush(); + } + } + + /** + * Record a new access entry. + */ + public function recordAccess(User $user, int $courseId, int $sessionId, string $ip): void + { + $access = new TrackECourseAccess(); + $access->setUser($user); + $access->setCId($courseId); + $access->setSessionId($sessionId); + $access->setUserIp($ip); + $access->setLoginCourseDate(new \DateTime()); + $access->setCounter(1); + $this->_em->persist($access); + $this->_em->flush(); + } + + /** + * Log out user access to a course. + */ + public function logoutAccess(User $user, int $courseId, int $sessionId, string $ip): void + { + $now = new DateTime("now", new \DateTimeZone("UTC")); + $sessionLifetime = 3600; + $limitTime = (new DateTime())->setTimestamp(time() - $sessionLifetime); + + $access = $this->createQueryBuilder('a') + ->where('a.user = :user AND a.cId = :courseId AND a.sessionId = :sessionId') + ->andWhere('a.loginCourseDate > :limitTime') + ->setParameters([ + 'user' => $user, + 'courseId' => $courseId, + 'sessionId' => $sessionId, + 'limitTime' => $limitTime, + ]) + ->orderBy('a.loginCourseDate', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + + if ($access) { + $access->setLogoutCourseDate($now); + $access->setCounter($access->getCounter() + 1); + $this->_em->flush(); + } else { + // No access found or existing access is outside the session lifetime + // Insert new access record + $newAccess = new TrackECourseAccess(); + $newAccess->setUser($user); + $newAccess->setCId($courseId); + $newAccess->setSessionId($sessionId); + $newAccess->setUserIp($ip); + $newAccess->setLoginCourseDate($now); + $newAccess->setLogoutCourseDate($now); + $newAccess->setCounter(1); + $this->_em->persist($newAccess); + $this->_em->flush(); + } + } } diff --git a/src/CoreBundle/Resources/config/listeners.yml b/src/CoreBundle/Resources/config/listeners.yml index 5ace2e1c315..b666c15eace 100644 --- a/src/CoreBundle/Resources/config/listeners.yml +++ b/src/CoreBundle/Resources/config/listeners.yml @@ -18,7 +18,7 @@ services: # Sets the user access in a course listener Chamilo\CoreBundle\EventListener\CourseAccessListener: tags: - - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 6 } + - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } # Sets the user access in a course session listener