Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 79 additions & 96 deletions src/CoreBundle/Controller/CourseMaintenanceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1001,15 +1001,15 @@
if ($selectionMode) {
// Convert to the expected structure for filterCourseResources()
$safeSelected = [
'documents' => array_fill_keys(array_map('intval', array_keys($normSel['documents'] ?? [])), true),

Check failure on line 1004 in src/CoreBundle/Controller/CourseMaintenanceController.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test on ubuntu-latest

EmptyArrayAccess

src/CoreBundle/Controller/CourseMaintenanceController.php:1004:72: EmptyArrayAccess: Cannot access value on empty array variable (see https://psalm.dev/100)
'links' => array_fill_keys(array_map('intval', array_keys($normSel['links'] ?? [])), true),

Check failure on line 1005 in src/CoreBundle/Controller/CourseMaintenanceController.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test on ubuntu-latest

EmptyArrayAccess

src/CoreBundle/Controller/CourseMaintenanceController.php:1005:72: EmptyArrayAccess: Cannot access value on empty array variable (see https://psalm.dev/100)
'forums' => array_fill_keys(array_map('intval', array_keys($normSel['forums'] ?? [])), true),

Check failure on line 1006 in src/CoreBundle/Controller/CourseMaintenanceController.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test on ubuntu-latest

EmptyArrayAccess

src/CoreBundle/Controller/CourseMaintenanceController.php:1006:72: EmptyArrayAccess: Cannot access value on empty array variable (see https://psalm.dev/100)
];
// Also include expansions from categories
$fullSnapshot = isset($courseFull) ? $courseFull : $course;
$expandedAll = $this->expandCc13SelectionFromCategories($fullSnapshot, $normSel);
foreach (['documents','links','forums'] as $k) {
foreach (array_keys($expandedAll[$k] ?? []) as $idStr) {

Check failure on line 1012 in src/CoreBundle/Controller/CourseMaintenanceController.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test on ubuntu-latest

NoValue

src/CoreBundle/Controller/CourseMaintenanceController.php:1012:68: NoValue: All possible types for this assignment were invalidated - This may be dead code (see https://psalm.dev/179)
$safeSelected[$k][(int)$idStr] = true;
}
}
Expand Down Expand Up @@ -3309,29 +3309,35 @@
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',
Expand All @@ -3341,96 +3347,70 @@
'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',
Expand All @@ -3444,11 +3424,14 @@
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));
}

/**
Expand Down
96 changes: 64 additions & 32 deletions src/CourseBundle/Component/CourseCopy/CourseRestorer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();

Expand All @@ -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) {
Expand All @@ -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();

Expand All @@ -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();
}

Expand Down
Loading