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 1ae000a4772..fbe96b549ff 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; @@ -39,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 ) {} /** @@ -268,6 +270,20 @@ public function cleanSessionHandler(Request $request): void ChamiloSession::erase('course_already_visited'); } + $courseId = $sessionHandler->get('cid', 0); + $sessionId = $sessionHandler->get('sid', 0); + $ip = $request->getClientIp(); + if ($courseId !== 0) { + $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'); $sessionHandler->remove('cid'); diff --git a/src/CoreBundle/EventListener/CourseAccessListener.php b/src/CoreBundle/EventListener/CourseAccessListener.php index 6091639e209..364b155344c 100644 --- a/src/CoreBundle/EventListener/CourseAccessListener.php +++ b/src/CoreBundle/EventListener/CourseAccessListener.php @@ -7,10 +7,14 @@ 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 Chamilo\CoreBundle\ServiceHelper\UserHelper; +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,31 +22,38 @@ */ 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 CidReqHelper $cidReqHelper, + private readonly UserHelper $userHelper + ) {} - public function __invoke(CourseAccess $event): void + public function onKernelRequest(RequestEvent $event): void { - // CourseAccess - $user = $event->getUser(); - $course = $event->getCourse(); - $ip = $this->request->getClientIp(); - - $access = new TrackECourseAccess(); - $access - ->setCId($course->getId()) - ->setUser($user) - ->setSessionId(0) - ->setUserIp($ip) - ; - - $this->em->persist($access); - $this->em->flush(); + 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->userHelper->getCurrent(); + if ($user) { + $ip = $event->getRequest()->getClientIp(); + $accessRepository = $this->em->getRepository(TrackECourseAccess::class); + $access = $accessRepository->findExistingAccess($user, $courseId, $sessionId); + + if ($access) { + $accessRepository->updateAccess($access); + } else { + $accessRepository->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); + } + } } } 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 c3b234ae970..b666c15eace 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 } + - # 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}