diff --git a/public/plugin/EmbedRegistry/EmbedRegistryPlugin.php b/public/plugin/EmbedRegistry/EmbedRegistryPlugin.php index bccc3e5ba1a..3824abb6c9d 100644 --- a/public/plugin/EmbedRegistry/EmbedRegistryPlugin.php +++ b/public/plugin/EmbedRegistry/EmbedRegistryPlugin.php @@ -3,6 +3,8 @@ use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Session; +use Chamilo\CoreBundle\Entity\TrackEAccess; +use Chamilo\CourseBundle\Entity\CTool; use Chamilo\PluginBundle\EmbedRegistry\Entity\Embed; use Doctrine\ORM\Tools\SchemaTool; @@ -61,8 +63,9 @@ public static function create() } /** - * @throws \Doctrine\ORM\Tools\ToolsException + * Create DB schema for this plugin if not present. * + * @throws \Doctrine\ORM\Tools\ToolsException * @throws \Doctrine\DBAL\Exception */ public function install() @@ -74,14 +77,12 @@ public function install() } $schemaTool = new SchemaTool($em); - $schemaTool->createSchema( - [ - $em->getClassMetadata(Embed::class), - ] - ); + $schemaTool->createSchema([$em->getClassMetadata(Embed::class)]); } /** + * Drop DB schema if present. + * * @throws \Doctrine\DBAL\Exception */ public function uninstall() @@ -93,14 +94,12 @@ public function uninstall() } $schemaTool = new SchemaTool($em); - $schemaTool->dropSchema( - [ - $em->getClassMetadata(Embed::class), - ] - ); + $schemaTool->dropSchema([$em->getClassMetadata(Embed::class)]); } /** + * After (re)configuring the plugin, (re)create tool links if enabled. + * * @return EmbedRegistryPlugin */ public function performActionsAfterConfigure() @@ -110,7 +109,8 @@ public function performActionsAfterConfigure() $this->deleteCourseToolLinks(); if ('true' === $this->get(self::SETTING_ENABLED)) { - $courses = $em->createQuery('SELECT c.id FROM ChamiloCoreBundle:Course c')->getResult(); + // Use FQCN instead of "ChamiloCoreBundle:Course". + $courses = $em->createQuery('SELECT c.id FROM '.Course::class.' c')->getResult(); foreach ($courses as $course) { $this->createLinkToCourseTool($this->getToolTitle(), $course['id']); @@ -121,69 +121,93 @@ public function performActionsAfterConfigure() } /** + * Hook called when a course is deleted. + * We only have the course ID here, but e.course is a relation, + * so we must compare using IDENTITY(e.course) = :courseId. + * * @param int $courseId */ public function doWhenDeletingCourse($courseId) { - Database::getManager() - ->createQuery('DELETE FROM ChamiloPluginBundle:EmbedRegistry\Embed e WHERE e.course = :course') - ->execute(['course' => (int) $courseId]); + $em = Database::getManager(); + + // IMPORTANT: Use FQCN and IDENTITY() for relation-to-id comparison. + $em->createQuery( + 'DELETE FROM '.Embed::class.' e WHERE IDENTITY(e.course) = :courseId' + ) + ->setParameter('courseId', (int) $courseId) + ->execute(); } /** + * Hook called when a session is deleted. + * We only have the session ID here, but e.session is a relation, + * so we must compare using IDENTITY(e.session) = :sessionId. + * * @param int $sessionId */ public function doWhenDeletingSession($sessionId) { - Database::getManager() - ->createQuery('DELETE FROM ChamiloPluginBundle:EmbedRegistry\Embed e WHERE e.session = :session') - ->execute(['session' => (int) $sessionId]); + $em = Database::getManager(); + + // IMPORTANT: Use FQCN and IDENTITY() for relation-to-id comparison. + $em->createQuery( + 'DELETE FROM '.Embed::class.' e WHERE IDENTITY(e.session) = :sessionId' + ) + ->setParameter('sessionId', (int) $sessionId) + ->execute(); } /** + * Get the currently active embed (by date range) for a course and optional session. + * DO NOT compare an entity relation to a scalar id in DQL; bind the entity itself. + * * @throws \Doctrine\ORM\NonUniqueResultException * - * @return Embed + * @return Embed|null */ public function getCurrentEmbed(Course $course, Session $session = null) { - $embedRepo = Database::getManager()->getRepository('ChamiloPluginBundle:EmbedRegistry\Embed'); - $qb = $embedRepo->createQueryBuilder('e'); - $query = $qb + $em = Database::getManager(); + $repo = $em->getRepository(Embed::class); + $qb = $repo->createQueryBuilder('e'); + + $qb ->where('e.displayStartDate <= :now') ->andWhere('e.displayEndDate >= :now') - ->andWhere( - $qb->expr()->eq('e.course', $course->getId()) - ); - - $query->andWhere( - $session - ? $qb->expr()->eq('e.session', $session->getId()) - : $qb->expr()->isNull('e.session') - ); + ->andWhere('e.course = :course') + ->setParameter('now', new \DateTimeImmutable('now', new \DateTimeZone('UTC'))) + ->setParameter('course', $course); + + if ($session) { + $qb->andWhere('e.session = :session') + ->setParameter('session', $session); + } else { + $qb->andWhere('e.session IS NULL'); + } - $query = $query + return $qb ->orderBy('e.displayStartDate', 'DESC') ->setMaxResults(1) - ->setParameters(['now' => api_get_utc_datetime(null, false, true)]) - ->getQuery(); - - return $query->getOneOrNullResult(); + ->getQuery() + ->getOneOrNullResult(); } /** + * Human-readable date range for an embed. + * * @return string */ public function formatDisplayDate(Embed $embed) { $startDate = sprintf( '', - $embed->getDisplayStartDate()->format(DateTime::W3C), + $embed->getDisplayStartDate()->format(\DateTimeInterface::W3C), api_convert_and_format_date($embed->getDisplayStartDate()) ); $endDate = sprintf( '', - $embed->getDisplayEndDate()->format(DateTime::W3C), + $embed->getDisplayEndDate()->format(\DateTimeInterface::W3C), api_convert_and_format_date($embed->getDisplayEndDate()) ); @@ -191,6 +215,8 @@ public function formatDisplayDate(Embed $embed) } /** + * URL to view a single embed in the plugin. + * * @return string */ public function getViewUrl(Embed $embed) @@ -199,39 +225,43 @@ public function getViewUrl(Embed $embed) } /** + * Count distinct users who accessed this plugin within the embed date window. + * NOTE: TrackEAccess uses scalar fields (cId, accessSessionId), so pass integers. + * * @throws \Doctrine\ORM\Query\QueryException * * @return int */ public function getMembersCount(Embed $embed) { - $dql = 'SELECT COUNT(DISTINCT tea.accessUserId) FROM ChamiloCoreBundle:TrackEAccess tea + $dql = 'SELECT COUNT(DISTINCT tea.accessUserId) FROM '.TrackEAccess::class.' tea WHERE tea.accessTool = :tool AND (tea.accessDate >= :start_date AND tea.accessDate <= :end_date) AND - tea.cId = :course'; + tea.cId = :courseId'; $params = [ 'tool' => 'plugin_'.$this->get_name(), 'start_date' => $embed->getDisplayStartDate(), 'end_date' => $embed->getDisplayEndDate(), - 'course' => $embed->getCourse(), + // IMPORTANT: cId is an integer field, not a relation. + 'courseId' => $embed->getCourse()->getId(), ]; if ($embed->getSession()) { - $dql .= ' AND tea.accessSessionId = :session '; - - $params['session'] = $embed->getSession(); + $dql .= ' AND tea.accessSessionId = :sessionId '; + $params['sessionId'] = $embed->getSession()->getId(); } - $count = Database::getManager() + return (int) Database::getManager() ->createQuery($dql) ->setParameters($params) ->getSingleScalarResult(); - - return $count; } + /** + * Track a plugin access event (raw SQL-level insert is fine here). + */ public function saveEventAccessTool() { $tableAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ACCESS); @@ -246,11 +276,18 @@ public function saveEventAccessTool() Database::insert($tableAccess, $params); } + /** + * Remove tool links created for this plugin in course tools. + * Use FQCN (CTool::class) instead of "ChamiloCourseBundle:CTool". + */ private function deleteCourseToolLinks() { Database::getManager() - ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link') - ->execute(['category' => 'plugin', 'link' => 'EmbedRegistry/start.php%']); + ->createQuery( + 'DELETE FROM '.CTool::class.' t WHERE t.category = :category AND t.link LIKE :link' + ) + ->setParameters(['category' => 'plugin', 'link' => 'EmbedRegistry/start.php%']) + ->execute(); } public function get_name()