diff --git a/src/CoreBundle/Controller/CourseMaintenanceController.php b/src/CoreBundle/Controller/CourseMaintenanceController.php index a71ecff98ad..a91113941a5 100644 --- a/src/CoreBundle/Controller/CourseMaintenanceController.php +++ b/src/CoreBundle/Controller/CourseMaintenanceController.php @@ -3309,29 +3309,35 @@ private function normalizeBucketsForRestorer(object $course): void return; } - // Canonical keys -> constants used by the restorer (reference only) - $allowed = [ - 'link' => \defined('RESOURCE_LINK') ? RESOURCE_LINK : null, - 'link_category' => \defined('RESOURCE_LINKCATEGORY') ? RESOURCE_LINKCATEGORY : null, - 'forum' => \defined('RESOURCE_FORUM') ? RESOURCE_FORUM : null, - 'forum_category' => \defined('RESOURCE_FORUMCATEGORY') ? RESOURCE_FORUMCATEGORY : null, - 'forum_topic' => \defined('RESOURCE_FORUMTOPIC') ? RESOURCE_FORUMTOPIC : null, - 'forum_post' => \defined('RESOURCE_FORUMPOST') ? RESOURCE_FORUMPOST : null, - 'document' => \defined('RESOURCE_DOCUMENT') ? RESOURCE_DOCUMENT : null, - 'quiz' => \defined('RESOURCE_QUIZ') ? RESOURCE_QUIZ : null, - 'exercise_question' => \defined('RESOURCE_QUIZQUESTION') ? RESOURCE_QUIZQUESTION : null, - 'survey' => \defined('RESOURCE_SURVEY') ? RESOURCE_SURVEY : null, - 'survey_question' => \defined('RESOURCE_SURVEYQUESTION') ? RESOURCE_SURVEYQUESTION : null, - 'tool_intro' => \defined('RESOURCE_TOOL_INTRO') ? RESOURCE_TOOL_INTRO : null, - ]; + // Split meta buckets + $all = $course->resources; + $meta = []; + foreach ($all as $k => $v) { + if (\is_string($k) && str_starts_with($k, '__')) { + $meta[$k] = $v; + unset($all[$k]); + } + } - // Minimal, well-scoped alias map (input -> canonical) - $alias = [ - // docs - 'documents' => 'document', - 'document' => 'document', - 'document ' => 'document', - 'Document' => 'document', + // Start from current + $out = $all; + + // merge array buckets preserving numeric/string ids + $merge = static function (array $dst, array $src): array { + foreach ($src as $id => $obj) { + if (!\array_key_exists($id, $dst)) { + $dst[$id] = $obj; + } + } + return $dst; + }; + + // safe alias map (input -> canonical). Extend only if needed. + $aliases = [ + // documents + 'documents' => 'document', + 'Document' => 'document', + 'document ' => 'document', // tool intro 'tool introduction' => 'tool_intro', @@ -3341,96 +3347,70 @@ private function normalizeBucketsForRestorer(object $course): void 'Tool introduction' => 'tool_intro', // forums - 'forum' => 'forum', - 'forums' => 'forum', - 'forum_category' => 'forum_category', - 'Forum_Category' => 'forum_category', - 'forumcategory' => 'forum_category', - 'forum_topic' => 'forum_topic', - 'forumtopic' => 'forum_topic', - 'thread' => 'forum_topic', - 'forum_post' => 'forum_post', - 'forumpost' => 'forum_post', - 'post' => 'forum_post', + 'forums' => 'forum', + 'Forum' => 'forum', + 'Forum_Category' => 'forum_category', + 'forumcategory' => 'forum_category', + 'thread' => 'forum_topic', + 'Thread' => 'forum_topic', + 'forumtopic' => 'forum_topic', + 'post' => 'forum_post', + 'Post' => 'forum_post', + 'forumpost' => 'forum_post', // links - 'link' => 'link', - 'links' => 'link', - 'link_category' => 'link_category', - 'link category' => 'link_category', + 'links' => 'link', + 'link category' => 'link_category', // quiz + questions - 'quiz' => 'quiz', - 'exercise_question' => 'exercise_question', - 'Exercise_Question' => 'exercise_question', - 'exercisequestion' => 'exercise_question', + 'Exercise_Question' => 'exercise_question', + 'exercisequestion' => 'exercise_question', // surveys - 'survey' => 'survey', - 'surveys' => 'survey', - 'survey_question' => 'survey_question', - 'surveyquestion' => 'survey_question', - ]; - - $before = $course->resources; - - // Keep meta buckets verbatim - $meta = []; - foreach ($before as $k => $v) { - if (\is_string($k) && str_starts_with($k, '__')) { - $meta[$k] = $v; - unset($before[$k]); - } - } - - $hadDocument = - isset($before['document']) || - isset($before['Document']) || - isset($before['documents']); + 'surveys' => 'survey', + 'surveyquestion' => 'survey_question', - // Merge helper (preserve numeric/string ids) - $merge = static function (array $dst, array $src): array { - foreach ($src as $id => $obj) { - if (!array_key_exists($id, $dst)) { - $dst[$id] = $obj; - } - } - return $dst; - }; - - $out = []; + // announcements + 'announcements' => 'announcement', + 'Announcements' => 'announcement', + ]; - foreach ($before as $rawKey => $bucket) { - if (!\is_array($bucket)) { - // Unexpected shape; skip silently (defensive) - continue; + // Normalize keys (case/spacing) and apply alias merges + foreach ($all as $rawKey => $_bucket) { + if (!\is_array($_bucket)) { + continue; // defensive } - - // Normalize key shape first $k = (string) $rawKey; - $norm = strtolower(trim($k)); - $norm = strtr($norm, ['\\' => '/', '-' => '_']); // cheap normalization - // Map via alias table if present - $canon = $alias[$norm] ?? $alias[str_replace('/', '_', $norm)] ?? null; + $norm = strtolower(trim(strtr($k, ['\\' => '/', '-' => '_']))); + $norm2 = str_replace('/', '_', $norm); - // If still unknown, try a sane guess: underscores + lowercase - if (null === $canon) { - $guess = str_replace(['/', ' '], '_', $norm); - $canon = \array_key_exists($guess, $allowed) ? $guess : null; + $canonical = null; + if (isset($aliases[$norm])) { + $canonical = $aliases[$norm]; + } elseif (isset($aliases[$norm2])) { + $canonical = $aliases[$norm2]; } - // Only produce buckets with canonical keys we support; unknown keys are ignored here - if (null !== $canon && \array_key_exists($canon, $allowed)) { - $out[$canon] = isset($out[$canon]) ? $merge($out[$canon], $bucket) : $bucket; + if ($canonical && $canonical !== $rawKey) { + // Merge into canonical and drop the alias key + $out[$canonical] = isset($out[$canonical]) && \is_array($out[$canonical]) + ? $merge($out[$canonical], $_bucket) + : $_bucket; + unset($out[$rawKey]); } + // else: leave as-is (pass-through) } - // Hard safety net: if a "document" bucket existed before, ensure it remains. - if ($hadDocument && !isset($out['document'])) { - $out['document'] = (array) ($before['document'] ?? $before['Document'] ?? $before['documents'] ?? []); + // Safety: if there was any docs bucket under an alias, ensure 'document' is present. + if (!isset($out['document'])) { + if (isset($all['documents']) && \is_array($all['documents'])) { + $out['document'] = $all['documents']; + } elseif (isset($all['Document']) && \is_array($all['Document'])) { + $out['document'] = $all['Document']; + } } - // Gentle ordering to keep things readable + // Gentle ordering for readability only (does not affect presence) $order = [ 'announcement', 'document', 'link', 'link_category', 'forum', 'forum_category', 'forum_topic', 'forum_post', @@ -3444,11 +3424,14 @@ private function normalizeBucketsForRestorer(object $course): void uksort($out, static function ($a, $b) use ($w) { $wa = $w[$a] ?? 9999; $wb = $w[$b] ?? 9999; - return $wa <=> $wb ?: strcasecmp($a, $b); + return $wa <=> $wb ?: strcasecmp((string) $a, (string) $b); }); - // Final assign (meta first, then normalized buckets) + // Final assign: meta first, then normalized buckets $course->resources = $meta + $out; + + // Debug trace to verify we didn't lose keys + $this->logDebug('[normalizeBucketsForRestorer] final keys', array_keys((array) $course->resources)); } /** diff --git a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php index b270055a184..01e7726cfbd 100644 --- a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php +++ b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php @@ -1659,74 +1659,110 @@ public function restore_links($session_id = 0): void /** * Restore tool introductions. * + * Accept multiple bucket spellings to be robust against controller normalization: + * - RESOURCE_TOOL_INTRO (if defined) + * - 'Tool introduction' (legacy) + * - 'tool_intro' / 'tool introduction' / 'tool_introduction' + * * @param mixed $sessionId */ public function restore_tool_intro($sessionId = 0): void { $resources = $this->course->resources ?? []; + // Detect the right bucket key (be generous with aliases) $bagKey = null; - if ($this->course->has_resources(RESOURCE_TOOL_INTRO)) { - $bagKey = RESOURCE_TOOL_INTRO; - } elseif (!empty($resources['Tool introduction'])) { - $bagKey = 'Tool introduction'; + $candidates = []; + + if (\defined('RESOURCE_TOOL_INTRO')) { + $candidates[] = RESOURCE_TOOL_INTRO; } - if (null === $bagKey || empty($resources[$bagKey]) || !\is_array($resources[$bagKey])) { + + // Common spellings seen in exports / normalizers + $candidates = array_merge($candidates, [ + 'Tool introduction', + 'tool introduction', + 'tool_introduction', + 'tool/intro', + 'tool_intro', + ]); + + foreach ($candidates as $k) { + if (!empty($resources[$k]) && \is_array($resources[$k])) { + $bagKey = $k; + break; + } + } + + if (null === $bagKey) { + $this->dlog('restore_tool_intro: no matching bucket found', [ + 'available_keys' => array_keys((array) $resources), + ]); return; } $sessionId = (int) $sessionId; - $this->dlog('restore_tool_intro: begin', ['count' => \count($resources[$bagKey])]); + $this->dlog('restore_tool_intro: begin', [ + 'bucket' => $bagKey, + 'count' => \count($resources[$bagKey]), + ]); - $em = Database::getManager(); - $course = api_get_course_entity($this->destination_course_id); + $em = Database::getManager(); + $course = api_get_course_entity($this->destination_course_id); $session = $sessionId ? api_get_session_entity($sessionId) : null; - $toolRepo = $em->getRepository(Tool::class); - $cToolRepo = $em->getRepository(CTool::class); - $introRepo = $em->getRepository(CToolIntro::class); + $toolRepo = $em->getRepository(Tool::class); + $cToolRepo = $em->getRepository(CTool::class); + $introRepo = $em->getRepository(CToolIntro::class); foreach ($resources[$bagKey] as $rawId => $tIntro) { - // Resolve tool key + // Resolve tool key (id may be missing in some dumps) $toolKey = trim((string) ($tIntro->id ?? '')); if ('' === $toolKey || '0' === $toolKey) { $toolKey = (string) $rawId; } $alias = strtolower($toolKey); + + // Normalize common aliases to platform keys if ('homepage' === $alias || 'course_home' === $alias) { $toolKey = 'course_homepage'; } $this->dlog('restore_tool_intro: resolving tool key', [ - 'raw_id' => (string) $rawId, - 'obj_id' => isset($tIntro->id) ? (string) $tIntro->id : null, - 'toolKey' => $toolKey, + 'raw_id' => (string) $rawId, + 'obj_id' => isset($tIntro->id) ? (string) $tIntro->id : null, + 'toolKey' => $toolKey, ]); // Already mapped? $mapped = (int) ($tIntro->destination_id ?? 0); if ($mapped > 0) { - $this->dlog('restore_tool_intro: already mapped, skipping', ['src_id' => $toolKey, 'dst_id' => $mapped]); - + $this->dlog('restore_tool_intro: already mapped, skipping', [ + 'src_id' => $toolKey, 'dst_id' => $mapped, + ]); continue; } - // Rewrite HTML using the central helper + // Rewrite HTML using centralized helper (keeps document links consistent) $introHtml = $this->rewriteHtmlForCourse((string) ($tIntro->intro_text ?? ''), $sessionId, '[tool_intro.intro]'); - // Find platform Tool entity + // Find platform Tool entity by title; try a couple of fallbacks $toolEntity = $toolRepo->findOneBy(['title' => $toolKey]); + if (!$toolEntity) { + // Fallbacks: lower/upper case attempts + $toolEntity = $toolRepo->findOneBy(['title' => strtolower($toolKey)]) + ?: $toolRepo->findOneBy(['title' => ucfirst(strtolower($toolKey))]); + } if (!$toolEntity) { $this->dlog('restore_tool_intro: missing Tool entity, skipping', ['tool' => $toolKey]); - continue; } - // Find or create course tool (CTool) + // Ensure a CTool exists for this course/session+tool $cTool = $cToolRepo->findOneBy([ - 'course' => $course, + 'course' => $course, 'session' => $session, - 'title' => $toolKey, + 'title' => $toolKey, ]); if (!$cTool) { @@ -1739,9 +1775,7 @@ public function restore_tool_intro($sessionId = 0): void ->setVisibility(true) ->setParent($course) ->setCreator($course->getCreator() ?? null) - ->addCourseLink($course) - ; - + ->addCourseLink($course, $session); $em->persist($cTool); $em->flush(); @@ -1751,7 +1785,7 @@ public function restore_tool_intro($sessionId = 0): void ]); } - // Intro entity (create/overwrite/skip according to policy) + // Create/overwrite intro according to file policy $intro = $introRepo->findOneBy(['courseTool' => $cTool]); if ($intro) { @@ -1774,9 +1808,7 @@ public function restore_tool_intro($sessionId = 0): void $intro = (new CToolIntro()) ->setCourseTool($cTool) ->setIntroText($introHtml) - ->setParent($course) - ; - + ->setParent($course); $em->persist($intro); $em->flush(); @@ -1786,8 +1818,8 @@ public function restore_tool_intro($sessionId = 0): void ]); } - // Map destination id back - $this->course->resources[$bagKey][$rawId] ??= new stdClass(); + // Map destination id back into the bucket used + $this->course->resources[$bagKey][$rawId] ??= new \stdClass(); $this->course->resources[$bagKey][$rawId]->destination_id = (int) $intro->getIid(); }