From c6f5c5e962a1aec328e5c4d63f67efa9b05c763c Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Sat, 24 Jan 2026 02:41:39 -0500 Subject: [PATCH] Course: Fix course tools API: handle missing plugin tool handlers & normalize tool names - refs #7415 --- public/plugin/H5pImport/H5pImportPlugin.php | 8 +- src/CoreBundle/State/CToolStateProvider.php | 112 ++++++++++++++++++-- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/public/plugin/H5pImport/H5pImportPlugin.php b/public/plugin/H5pImport/H5pImportPlugin.php index 59e04b1fa92..1196d121611 100644 --- a/public/plugin/H5pImport/H5pImportPlugin.php +++ b/public/plugin/H5pImport/H5pImportPlugin.php @@ -287,11 +287,11 @@ private function addCourseTools(): void } } - private function deleteCourseToolLinks() + private function deleteCourseToolLinks(): void { - Database::getManager() - ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link') - ->execute(['category' => 'authoring', 'link' => '../plugin/H5pImport/start.php%']); + $em = Database::getManager(); + $em->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.title = :title') + ->execute(['title' => 'H5P import']); } /** diff --git a/src/CoreBundle/State/CToolStateProvider.php b/src/CoreBundle/State/CToolStateProvider.php index cbf02aad46b..5993330dedb 100644 --- a/src/CoreBundle/State/CToolStateProvider.php +++ b/src/CoreBundle/State/CToolStateProvider.php @@ -23,6 +23,7 @@ use Event; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\RequestStack; +use Throwable; /** * @template-implements ProviderInterface @@ -72,25 +73,39 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $session = $this->getSession(); $course = $this->getCourse(); - [$restrictToPositioning, $allowedToolName] = $this->shouldRestrictToPositioningOnly($user, $course->getId(), $session?->getId()); + [$restrictToPositioning, $allowedToolName] = $this->shouldRestrictToPositioningOnly( + $user, + $course->getId(), + $session?->getId() + ); $results = []; /** @var CTool $cTool */ foreach ($result as $cTool) { - if ($restrictToPositioning && $cTool->getTool()->getTitle() !== $allowedToolName) { + $resolved = $this->resolveToolModelFromCTool($cTool); + if (null === $resolved) { continue; } - $toolModel = $this->toolChain->getToolFromName( - $cTool->getTool()->getTitle() - ); + $toolModel = $resolved['model']; + $resolvedName = $resolved['name']; + + // If a positioning restriction is active, keep only the allowed tool. + if ($restrictToPositioning && $allowedToolName && $resolvedName !== $allowedToolName) { + continue; + } if (!$isAllowToEdit && 'admin' === $toolModel->getCategory()) { continue; } - $resourceLinks = $cTool->getResourceNode()->getResourceLinks(); + $resourceNode = $cTool->getResourceNode(); + if (!$resourceNode) { + continue; + } + + $resourceLinks = $resourceNode->getResourceLinks(); if ($session && $allowVisibilityInSession) { $sessionLink = $resourceLinks->findFirst( @@ -109,8 +124,12 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } if (!$isAllowToEdit || 'studentview' === $studentView) { - $notPublishedLink = ResourceLink::VISIBILITY_PUBLISHED !== $resourceLinks->first()->getVisibility(); + $firstLink = $resourceLinks->first(); + if (!$firstLink) { + continue; + } + $notPublishedLink = ResourceLink::VISIBILITY_PUBLISHED !== $firstLink->getVisibility(); if ($notPublishedLink) { continue; } @@ -122,6 +141,85 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return $results; } + /** + * Resolve a ToolChain model for a given CTool safely. + * Tries multiple candidate names derived from the stored tool title. + * + * @return array{model: object, name: string}|null + */ + private function resolveToolModelFromCTool(CTool $cTool): ?array + { + $toolEntity = $cTool->getTool(); + $rawTitle = $toolEntity ? (string) $toolEntity->getTitle() : ''; + + foreach ($this->buildToolNameCandidates($rawTitle) as $candidate) { + try { + $model = $this->toolChain->getToolFromName($candidate); + + // Return the candidate we used so other logic can rely on a stable key. + return [ + 'model' => $model, + 'name' => $candidate, + ]; + } catch (Throwable) { + // Try next candidate + } + } + + return null; + } + + /** + * Build candidate tool names from a DB title. + * This keeps backward compatibility while supporting human titles like "H5P import". + * + * @return string[] + */ + private function buildToolNameCandidates(string $rawTitle): array + { + $rawTitle = trim($rawTitle); + if ('' === $rawTitle) { + return []; + } + + $candidates = []; + + // Prefer lowercase first (ToolChain commonly uses lowercase keys) + $lower = strtolower($rawTitle); + $candidates[] = $lower; + + // Original as fallback + if ($rawTitle !== $lower) { + $candidates[] = $rawTitle; + } + + // Replace spaces/dashes with underscores (e.g., "H5P import" -> "h5p_import") + $spaceSnake = strtolower(preg_replace('/[\s\-]+/', '_', $rawTitle) ?? $rawTitle); + $spaceSnake = trim($spaceSnake, '_'); + $candidates[] = $spaceSnake; + + // Replace any non-alnum with underscores + $alnumSnake = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $rawTitle) ?? $rawTitle); + $alnumSnake = trim($alnumSnake, '_'); + $candidates[] = $alnumSnake; + + // CamelCase to snake_case (e.g., "CustomCertificate" -> "custom_certificate") + $camelSnake = preg_replace('/(? is_string($v) && '' !== trim($v)))); + + return $candidates; + } + private function shouldRestrictToPositioningOnly(?User $user, int $courseId, ?int $sessionId): array { if (!$user || !$user->isStudent()) {