2323use Event ;
2424use Symfony \Bundle \SecurityBundle \Security ;
2525use Symfony \Component \HttpFoundation \RequestStack ;
26+ use Throwable ;
2627
2728/**
2829 * @template-implements ProviderInterface<CTool>
@@ -72,25 +73,39 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7273 $ session = $ this ->getSession ();
7374 $ course = $ this ->getCourse ();
7475
75- [$ restrictToPositioning , $ allowedToolName ] = $ this ->shouldRestrictToPositioningOnly ($ user , $ course ->getId (), $ session ?->getId());
76+ [$ restrictToPositioning , $ allowedToolName ] = $ this ->shouldRestrictToPositioningOnly (
77+ $ user ,
78+ $ course ->getId (),
79+ $ session ?->getId()
80+ );
7681
7782 $ results = [];
7883
7984 /** @var CTool $cTool */
8085 foreach ($ result as $ cTool ) {
81- if ($ restrictToPositioning && $ cTool ->getTool ()->getTitle () !== $ allowedToolName ) {
86+ $ resolved = $ this ->resolveToolModelFromCTool ($ cTool );
87+ if (null === $ resolved ) {
8288 continue ;
8389 }
8490
85- $ toolModel = $ this ->toolChain ->getToolFromName (
86- $ cTool ->getTool ()->getTitle ()
87- );
91+ $ toolModel = $ resolved ['model ' ];
92+ $ resolvedName = $ resolved ['name ' ];
93+
94+ // If a positioning restriction is active, keep only the allowed tool.
95+ if ($ restrictToPositioning && $ allowedToolName && $ resolvedName !== $ allowedToolName ) {
96+ continue ;
97+ }
8898
8999 if (!$ isAllowToEdit && 'admin ' === $ toolModel ->getCategory ()) {
90100 continue ;
91101 }
92102
93- $ resourceLinks = $ cTool ->getResourceNode ()->getResourceLinks ();
103+ $ resourceNode = $ cTool ->getResourceNode ();
104+ if (!$ resourceNode ) {
105+ continue ;
106+ }
107+
108+ $ resourceLinks = $ resourceNode ->getResourceLinks ();
94109
95110 if ($ session && $ allowVisibilityInSession ) {
96111 $ sessionLink = $ resourceLinks ->findFirst (
@@ -109,8 +124,12 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
109124 }
110125
111126 if (!$ isAllowToEdit || 'studentview ' === $ studentView ) {
112- $ notPublishedLink = ResourceLink::VISIBILITY_PUBLISHED !== $ resourceLinks ->first ()->getVisibility ();
127+ $ firstLink = $ resourceLinks ->first ();
128+ if (!$ firstLink ) {
129+ continue ;
130+ }
113131
132+ $ notPublishedLink = ResourceLink::VISIBILITY_PUBLISHED !== $ firstLink ->getVisibility ();
114133 if ($ notPublishedLink ) {
115134 continue ;
116135 }
@@ -122,6 +141,85 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
122141 return $ results ;
123142 }
124143
144+ /**
145+ * Resolve a ToolChain model for a given CTool safely.
146+ * Tries multiple candidate names derived from the stored tool title.
147+ *
148+ * @return array{model: object, name: string}|null
149+ */
150+ private function resolveToolModelFromCTool (CTool $ cTool ): ?array
151+ {
152+ $ toolEntity = $ cTool ->getTool ();
153+ $ rawTitle = $ toolEntity ? (string ) $ toolEntity ->getTitle () : '' ;
154+
155+ foreach ($ this ->buildToolNameCandidates ($ rawTitle ) as $ candidate ) {
156+ try {
157+ $ model = $ this ->toolChain ->getToolFromName ($ candidate );
158+
159+ // Return the candidate we used so other logic can rely on a stable key.
160+ return [
161+ 'model ' => $ model ,
162+ 'name ' => $ candidate ,
163+ ];
164+ } catch (Throwable ) {
165+ // Try next candidate
166+ }
167+ }
168+
169+ return null ;
170+ }
171+
172+ /**
173+ * Build candidate tool names from a DB title.
174+ * This keeps backward compatibility while supporting human titles like "H5P import".
175+ *
176+ * @return string[]
177+ */
178+ private function buildToolNameCandidates (string $ rawTitle ): array
179+ {
180+ $ rawTitle = trim ($ rawTitle );
181+ if ('' === $ rawTitle ) {
182+ return [];
183+ }
184+
185+ $ candidates = [];
186+
187+ // Prefer lowercase first (ToolChain commonly uses lowercase keys)
188+ $ lower = strtolower ($ rawTitle );
189+ $ candidates [] = $ lower ;
190+
191+ // Original as fallback
192+ if ($ rawTitle !== $ lower ) {
193+ $ candidates [] = $ rawTitle ;
194+ }
195+
196+ // Replace spaces/dashes with underscores (e.g., "H5P import" -> "h5p_import")
197+ $ spaceSnake = strtolower (preg_replace ('/[\s\-]+/ ' , '_ ' , $ rawTitle ) ?? $ rawTitle );
198+ $ spaceSnake = trim ($ spaceSnake , '_ ' );
199+ $ candidates [] = $ spaceSnake ;
200+
201+ // Replace any non-alnum with underscores
202+ $ alnumSnake = strtolower (preg_replace ('/[^a-z0-9]+/i ' , '_ ' , $ rawTitle ) ?? $ rawTitle );
203+ $ alnumSnake = trim ($ alnumSnake , '_ ' );
204+ $ candidates [] = $ alnumSnake ;
205+
206+ // CamelCase to snake_case (e.g., "CustomCertificate" -> "custom_certificate")
207+ $ camelSnake = preg_replace ('/(?<!^)[A-Z]/ ' , '_$0 ' , $ rawTitle ) ?? $ rawTitle ;
208+ $ camelSnake = strtolower ($ camelSnake );
209+ $ camelSnake = strtolower (preg_replace ('/[^a-z0-9]+/i ' , '_ ' , $ camelSnake ) ?? $ camelSnake );
210+ $ camelSnake = trim ($ camelSnake , '_ ' );
211+ $ candidates [] = $ camelSnake ;
212+
213+ // Some tool keys might be stored without underscores
214+ $ candidates [] = str_replace ('_ ' , '' , $ camelSnake );
215+ $ candidates [] = str_replace ('_ ' , '' , $ alnumSnake );
216+
217+ // Unique + non-empty
218+ $ candidates = array_values (array_unique (array_filter ($ candidates , static fn ($ v ) => is_string ($ v ) && '' !== trim ($ v ))));
219+
220+ return $ candidates ;
221+ }
222+
125223 private function shouldRestrictToPositioningOnly (?User $ user , int $ courseId , ?int $ sessionId ): array
126224 {
127225 if (!$ user || !$ user ->isStudent ()) {
0 commit comments