diff --git a/src/CoreBundle/Controller/CourseMaintenanceController.php b/src/CoreBundle/Controller/CourseMaintenanceController.php index fb621161688..a71ecff98ad 100644 --- a/src/CoreBundle/Controller/CourseMaintenanceController.php +++ b/src/CoreBundle/Controller/CourseMaintenanceController.php @@ -138,10 +138,8 @@ public function importResources(int $node, string $backupId, Request $req): Json $this->setDebugFromRequest($req); $mode = strtolower((string) $req->query->get('mode', 'auto')); // 'auto' | 'dat' | 'moodle' - // Reutilizas TU loader actual con el nuevo flag $course = $this->loadLegacyCourseForAnyBackup($backupId, $mode === 'dat' ? 'chamilo' : $mode); - // Lo demás igual $this->logDebug('[importResources] course loaded', [ 'has_resources' => \is_array($course->resources ?? null), 'keys' => array_keys((array) ($course->resources ?? [])), @@ -178,6 +176,9 @@ public function importRestore( try { $payload = json_decode($req->getContent() ?: '{}', true); + // Keep mode consistent with GET /import/{backupId}/resources + $mode = strtolower((string) ($payload['mode'] ?? 'auto')); + $importOption = (string) ($payload['importOption'] ?? 'full_backup'); $sameFileNameOption = (int) ($payload['sameFileNameOption'] ?? 2); @@ -192,24 +193,28 @@ public function importRestore( 'sameFileNameOption' => $sameFileNameOption, 'selectedTypes' => $selectedTypes, 'hasResourcesMap' => !empty($selectedResources), + 'mode' => $mode, ]); - $course = $this->loadLegacyCourseForAnyBackup($backupId); + // Load with same mode to avoid switching source on POST + $course = $this->loadLegacyCourseForAnyBackup($backupId, $mode === 'dat' ? 'chamilo' : $mode); if (!\is_object($course) || empty($course->resources) || !\is_array($course->resources)) { return $this->json(['error' => 'Backup has no resources'], 400); } - $resourcesAll = (array) $course->resources; + $resourcesAll = $course->resources; $this->logDebug('[importRestore] BEFORE filter keys', array_keys($resourcesAll)); + // Always hydrate LP dependencies (even in full_backup). + $this->hydrateLpDependenciesFromSnapshot($course, $resourcesAll); + $this->logDebug('[importRestore] AFTER hydrate keys', array_keys((array) $course->resources)); + // Detect source BEFORE any filtering (meta may be dropped by filters) $importSource = $this->getImportSource($course); $isMoodle = ('moodle' === $importSource); $this->logDebug('[importRestore] detected import source', ['import_source' => $importSource, 'isMoodle' => $isMoodle]); if ('select_items' === $importOption) { - $this->hydrateLpDependenciesFromSnapshot($course, $resourcesAll); - if (empty($selectedResources) && !empty($selectedTypes)) { $selectedResources = $this->buildSelectionFromTypes($course, $selectedTypes); } @@ -218,7 +223,6 @@ public function importRestore( foreach ($selectedResources as $ids) { if (\is_array($ids) && !empty($ids)) { $hasAny = true; - break; } } @@ -237,7 +241,11 @@ public function importRestore( // NON-MOODLE if (!$isMoodle) { $this->logDebug('[importRestore] non-Moodle backup -> using CourseRestorer'); + // Trace around normalization to detect bucket drops + $this->logDebug('[importRestore] BEFORE normalize', array_keys((array) $course->resources)); + $this->normalizeBucketsForRestorer($course); + $this->logDebug('[importRestore] AFTER normalize', array_keys((array) $course->resources)); $restorer = new CourseRestorer($course); $restorer->set_file_option($this->mapSameNameOption($sameFileNameOption)); @@ -276,15 +284,16 @@ public function importRestore( $mark = static function (array &$dst, bool $cond, string $key): void { if ($cond) { $dst[$key] = true; } }; if ('full_backup' === $importOption) { + // Be tolerant with plural 'documents' $mark($wantedGroups, $present('link') || $present('link_category'), 'links'); $mark($wantedGroups, $present('forum') || $present('forum_category'), 'forums'); - $mark($wantedGroups, $present('document'), 'documents'); + $mark($wantedGroups, $present('document') || $present('documents'), 'documents'); $mark($wantedGroups, $present('quiz') || $present('exercise'), 'quizzes'); $mark($wantedGroups, $present('scorm'), 'scorm'); } else { $mark($wantedGroups, $present('link'), 'links'); $mark($wantedGroups, $present('forum') || $present('forum_category'), 'forums'); - $mark($wantedGroups, $present('document'), 'documents'); + $mark($wantedGroups, $present('document') || $present('documents'), 'documents'); $mark($wantedGroups, $present('quiz') || $present('exercise'), 'quizzes'); $mark($wantedGroups, $present('scorm'), 'scorm'); } @@ -3288,40 +3297,158 @@ private function loadLegacyCourseForAnyBackup(string $backupId, string $force = throw new \RuntimeException('Unsupported package: neither course_info.dat nor moodle_backup.xml found.'); } + /** + * Normalize resource buckets to the exact keys supported by CourseRestorer. + * Only the canonical keys below are produced; common aliases are mapped. + * - Never drop data: merge buckets; keep __meta as-is. + * - Make sure "document" survives if it existed before. + */ private function normalizeBucketsForRestorer(object $course): void { if (!isset($course->resources) || !\is_array($course->resources)) { return; } - $map = [ - 'link' => RESOURCE_LINK, - 'link_category' => RESOURCE_LINKCATEGORY, - 'forum' => RESOURCE_FORUM, - 'forum_category' => RESOURCE_FORUMCATEGORY, - 'forum_topic' => RESOURCE_FORUMTOPIC, - 'forum_post' => RESOURCE_FORUMPOST, - 'thread' => RESOURCE_FORUMTOPIC, - 'post' => RESOURCE_FORUMPOST, - 'document' => RESOURCE_DOCUMENT, - 'quiz' => RESOURCE_QUIZ, - 'exercise_question' => RESOURCE_QUIZQUESTION, - 'survey' => RESOURCE_SURVEY, - 'survey_question' => RESOURCE_SURVEYQUESTION, - 'tool_intro' => RESOURCE_TOOL_INTRO, + // 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, ]; - $res = $course->resources; - foreach ($map as $from => $to) { - if (isset($res[$from]) && \is_array($res[$from])) { - if (!isset($res[$to])) { - $res[$to] = $res[$from]; + // Minimal, well-scoped alias map (input -> canonical) + $alias = [ + // docs + 'documents' => 'document', + 'document' => 'document', + 'document ' => 'document', + 'Document' => 'document', + + // tool intro + 'tool introduction' => 'tool_intro', + 'tool_introduction' => 'tool_intro', + 'tool/introduction' => 'tool_intro', + 'tool intro' => 'tool_intro', + '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', + + // links + 'link' => 'link', + 'links' => 'link', + 'link_category' => 'link_category', + 'link category' => 'link_category', + + // quiz + questions + 'quiz' => 'quiz', + 'exercise_question' => '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']); + + // 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; } - unset($res[$from]); + } + return $dst; + }; + + $out = []; + + foreach ($before as $rawKey => $bucket) { + if (!\is_array($bucket)) { + // Unexpected shape; skip silently (defensive) + continue; + } + + // 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; + + // If still unknown, try a sane guess: underscores + lowercase + if (null === $canon) { + $guess = str_replace(['/', ' '], '_', $norm); + $canon = \array_key_exists($guess, $allowed) ? $guess : null; + } + + // 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; } } - $course->resources = $res; + // 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'] ?? []); + } + + // Gentle ordering to keep things readable + $order = [ + 'announcement', 'document', 'link', 'link_category', + 'forum', 'forum_category', 'forum_topic', 'forum_post', + 'quiz', 'exercise_question', + 'survey', 'survey_question', + 'learnpath', 'tool_intro', + 'work', + ]; + $w = []; + foreach ($order as $i => $key) { $w[$key] = $i; } + uksort($out, static function ($a, $b) use ($w) { + $wa = $w[$a] ?? 9999; + $wb = $w[$b] ?? 9999; + return $wa <=> $wb ?: strcasecmp($a, $b); + }); + + // Final assign (meta first, then normalized buckets) + $course->resources = $meta + $out; } /** diff --git a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php index 835a121bca2..b270055a184 100644 --- a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php +++ b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php @@ -418,29 +418,107 @@ public function restore_documents($session_id = 0, $respect_base_content = false return; } - $courseInfo = $this->destination_course_info; - $docRepo = Container::getDocumentRepository(); - $courseEntity = api_get_course_entity($courseInfo['real_id']); - $session = api_get_session_entity((int) $session_id); - $group = api_get_group_entity(0); + $courseInfo = $this->destination_course_info; + $docRepo = Container::getDocumentRepository(); + $courseEntity = api_get_course_entity($this->destination_course_id); + $session = api_get_session_entity((int) $session_id); + $group = api_get_group_entity(0); + + // Resolve the import root deterministically: + $resolveImportRoot = function (): string { + // explicit meta archiver_root + $metaRoot = (string) ($this->course->resources['__meta']['archiver_root'] ?? ''); + if ($metaRoot !== '' && is_dir($metaRoot) && (is_file($metaRoot.'/course_info.dat') || is_dir($metaRoot.'/document'))) { + $this->dlog('resolveImportRoot: using meta.archiver_root', ['dir' => $metaRoot]); + + return rtrim($metaRoot, '/'); + } + + // backup_path may be a dir or a zip + $bp = (string) ($this->course->backup_path ?? ''); + if ($bp !== '') { + if (is_dir($bp) && (is_file($bp.'/course_info.dat') || is_dir($bp.'/document'))) { + $this->dlog('resolveImportRoot: using backup_path (dir)', ['dir' => $bp]); + + return rtrim($bp, '/'); + } + + // if backup_path is a .zip, try to find its extracted sibling under the same folder + if (is_file($bp) && preg_match('/\.zip$/i', $bp)) { + $base = dirname($bp); + $cands = glob($base.'/CourseArchiver_*', GLOB_ONLYDIR) ?: []; + if (empty($cands) && is_dir($base)) { + // fallback in envs where glob is restricted + $tmp = array_diff(scandir($base) ?: [], ['.', '..']); + foreach ($tmp as $name) { + if (strpos($name, 'CourseArchiver_') === 0 && is_dir($base.'/'.$name)) { + $cands[] = $base.'/'.$name; + } + } + } + usort($cands, static function ($a, $b) { + return (filemtime($b) ?: 0) <=> (filemtime($a) ?: 0); + }); + foreach ($cands as $dir) { + if (is_file($dir.'/course_info.dat') || is_dir($dir.'/document')) { + $this->dlog('resolveImportRoot: using sibling CourseArchiver', ['dir' => $dir]); + // cache for later + $this->course->resources['__meta']['archiver_root'] = rtrim($dir, '/'); + + return rtrim($dir, '/'); + } + } + $this->dlog('resolveImportRoot: no sibling CourseArchiver found next to zip', ['base' => $base]); + } + } - // copyMode=false => import from backup_path (package) - $copyMode = empty($this->course->backup_path); - $srcRoot = $copyMode ? null : rtrim((string) $this->course->backup_path, '/').'/'; - $courseDir = $courseInfo['directory'] ?? $courseInfo['code'] ?? ''; + // scan del directorio oficial de backups del archiver + $scanBase = $this->getCourseBackupsBase(); + if (is_dir($scanBase)) { + $cands = glob($scanBase.'/CourseArchiver_*', GLOB_ONLYDIR) ?: []; + if (empty($cands)) { + $tmp = array_diff(scandir($scanBase) ?: [], ['.', '..']); + foreach ($tmp as $name) { + if (strpos($name, 'CourseArchiver_') === 0 && is_dir($scanBase.'/'.$name)) { + $cands[] = $scanBase.'/'.$name; + } + } + } + usort($cands, static function ($a, $b) { + return (filemtime($b) ?: 0) <=> (filemtime($a) ?: 0); + }); + foreach ($cands as $dir) { + if (is_file($dir.'/course_info.dat') || is_dir($dir.'/document')) { + $this->dlog('resolveImportRoot: using scanned CourseArchiver', ['dir' => $dir, 'scanBase' => $scanBase]); + $this->course->resources['__meta']['archiver_root'] = rtrim($dir, '/'); + + return rtrim($dir, '/'); + } + } + } + + $this->dlog('resolveImportRoot: no valid import root found, falling back to copy mode'); + + return ''; + }; + + $backupRoot = $resolveImportRoot(); + $copyMode = $backupRoot === ''; + $srcRoot = $copyMode ? null : ($backupRoot.'/'); $this->dlog('restore_documents: begin', [ - 'files' => \count($this->course->resources[RESOURCE_DOCUMENT] ?? []), + 'files' => \count($this->course->resources[RESOURCE_DOCUMENT] ?? []), 'session' => (int) $session_id, - 'mode' => $copyMode ? 'copy' : 'import', + 'mode' => $copyMode ? 'copy' : 'import', 'srcRoot' => $srcRoot, ]); $DBG = function (string $msg, array $ctx = []): void { + // Keep these concise to avoid noisy logs in production error_log('[RESTORE:HTMLURL] '.$msg.(empty($ctx) ? '' : ' '.json_encode($ctx))); }; - // Create folder chain under Documents (skipping "document" as root) and return destination parent iid + // Ensure a folder chain exists under Documents (skipping "document" as root) $ensureFolder = function (string $relPath) use ($docRepo, $courseEntity, $courseInfo, $session_id, $DBG) { $rel = '/'.ltrim($relPath, '/'); if ('/' === $rel || '' === $rel) { @@ -448,20 +526,20 @@ public function restore_documents($session_id = 0, $respect_base_content = false } $parts = array_values(array_filter(explode('/', trim($rel, '/')))); - // skip root "document" + // Skip "document" root if present $start = 0; if (isset($parts[0]) && 'document' === $parts[0]) { $start = 1; } - $accum = ''; + $accum = ''; $parentId = 0; for ($i = $start; $i < \count($parts); $i++) { - $seg = $parts[$i]; + $seg = $parts[$i]; $accum = $accum.'/'.$seg; $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity; - $title = $seg; + $title = $seg; $existing = $docRepo->findCourseResourceByTitle( $title, @@ -473,11 +551,10 @@ public function restore_documents($session_id = 0, $respect_base_content = false if ($existing) { $parentId = method_exists($existing, 'getIid') ? $existing->getIid() : 0; - continue; } - $entity = DocumentManager::addDocument( + $entity = DocumentManager::addDocument( ['real_id' => $courseInfo['real_id'], 'code' => $courseInfo['code']], $accum, 'folder', @@ -502,7 +579,7 @@ public function restore_documents($session_id = 0, $respect_base_content = false return $parentId; }; - // Robust HTML detection + // Robust HTML detection (extension sniff + small content probe + mimetype) $isHtmlFile = function (string $filePath, string $nameGuess): bool { $ext1 = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $ext2 = strtolower(pathinfo($nameGuess, PATHINFO_EXTENSION)); @@ -510,7 +587,7 @@ public function restore_documents($session_id = 0, $respect_base_content = false return true; } $peek = (string) @file_get_contents($filePath, false, null, 0, 2048); - if ('' === $peek) { + if ($peek === '') { return false; } $s = strtolower($peek); @@ -531,32 +608,33 @@ public function restore_documents($session_id = 0, $respect_base_content = false return false; }; - // Create folders coming in the backup (keep your original behavior, but skipping "document" root) + // Create folders found in the backup (keep behavior but skip "document" root) $folders = []; foreach ($this->course->resources[RESOURCE_DOCUMENT] as $k => $item) { if (FOLDER !== $item->file_type) { continue; } - $rel = '/'.ltrim(substr($item->path, 8), '/'); // remove "document" prefix - if ('/' === $rel) { + // Strip leading "document/" + $rel = '/'.ltrim(substr($item->path, 8), '/'); + if ($rel === '/') { continue; } - $parts = array_values(array_filter(explode('/', $rel))); - $accum = ''; + $parts = array_values(array_filter(explode('/', $rel))); + $accum = ''; $parentId = 0; foreach ($parts as $i => $seg) { $accum .= '/'.$seg; + if (isset($folders[$accum])) { $parentId = $folders[$accum]; - continue; } $parentResource = $parentId ? $docRepo->find($parentId) : $courseEntity; - $title = ($i === \count($parts) - 1) ? ($item->title ?: $seg) : $seg; + $title = ($i === \count($parts) - 1) ? ($item->title ?: $seg) : $seg; $existing = $docRepo->findCourseResourceByTitle( $title, @@ -599,37 +677,43 @@ public function restore_documents($session_id = 0, $respect_base_content = false } } - // GLOBAL PRE-SCAN with helper: build URL maps for all HTML dependencies (non-HTML files) - $urlMapByRel = []; + // GLOBAL PRE-SCAN: build URL maps for HTML dependencies (only in import-from-package mode) + $urlMapByRel = []; $urlMapByBase = []; foreach ($this->course->resources[RESOURCE_DOCUMENT] as $k => $item) { - if (DOCUMENT !== $item->file_type) { + if (DOCUMENT !== $item->file_type || $copyMode) { continue; } - if ($copyMode) { - continue; - } // only when importing from package $rawTitle = $item->title ?: basename((string) $item->path); - $srcPath = $srcRoot.$item->path; + $srcPath = $srcRoot.$item->path; + + // Fallback: if primary root is wrong, try archiver_root + if ((!is_file($srcPath) || !is_readable($srcPath))) { + $altRoot = rtrim((string) ($this->course->resources['__meta']['archiver_root'] ?? ''), '/').'/'; + if ($altRoot && $altRoot !== $srcRoot && is_readable($altRoot.$item->path)) { + $srcPath = $altRoot.$item->path; + $this->dlog('restore_documents: pre-scan fallback to alt root', ['src' => $srcPath]); + } + } + if (!is_file($srcPath) || !is_readable($srcPath)) { continue; } - // Only HTML if (!$isHtmlFile($srcPath, $rawTitle)) { continue; } $html = (string) @file_get_contents($srcPath); - if ('' === $html) { + if ($html === '') { continue; } $maps = ChamiloHelper::buildUrlMapForHtmlFromPackage( $html, - $courseDir, + ($courseInfo['directory'] ?? $courseInfo['code'] ?? ''), $srcRoot, $folders, $ensureFolder, @@ -642,7 +726,6 @@ public function restore_documents($session_id = 0, $respect_base_content = false $DBG ); - // Merge without overwriting previously resolved keys foreach ($maps['byRel'] as $kRel => $vUrl) { if (!isset($urlMapByRel[$kRel])) { $urlMapByRel[$kRel] = $vUrl; @@ -662,51 +745,58 @@ public function restore_documents($session_id = 0, $respect_base_content = false continue; } - $srcPath = null; + $srcPath = null; $rawTitle = $item->title ?: basename((string) $item->path); if ($copyMode) { + // Copy from existing document (legacy copy flow) $srcDoc = null; if (!empty($item->source_id)) { $srcDoc = $docRepo->find((int) $item->source_id); } if (!$srcDoc) { $this->dlog('restore_documents: source CDocument not found by source_id', ['source_id' => $item->source_id ?? null]); - continue; } $srcPath = $this->resourceFileAbsPathFromDocument($srcDoc); if (!$srcPath) { $this->dlog('restore_documents: source file not readable from ResourceFile', ['source_id' => (int) $item->source_id]); - continue; } } else { + // Import from extracted package $srcPath = $srcRoot.$item->path; + + // Fallback to archiver_root if primary root is wrong if (!is_file($srcPath) || !is_readable($srcPath)) { - $this->dlog('restore_documents: source file not found/readable', ['src' => $srcPath]); + $altRoot = rtrim((string) ($this->course->resources['__meta']['archiver_root'] ?? ''), '/').'/'; + if ($altRoot && $altRoot !== $srcRoot && is_readable($altRoot.$item->path)) { + $srcPath = $altRoot.$item->path; + $this->dlog('restore_documents: fallback to alt root', ['src' => $srcPath]); + } + } + if (!is_file($srcPath) || !is_readable($srcPath)) { + $this->dlog('restore_documents: source file not found/readable', ['src' => $srcPath]); continue; } } - $isHtml = $isHtmlFile($srcPath, $rawTitle); - - $rel = '/'.ltrim(substr($item->path, 8), '/'); // remove "document" prefix + $isHtml = $isHtmlFile($srcPath, $rawTitle); + $rel = '/'.ltrim(substr($item->path, 8), '/'); // remove "document" prefix $parentRel = rtrim(\dirname($rel), '/'); - $parentId = $folders[$parentRel] ?? 0; + $parentId = $folders[$parentRel] ?? 0; if (!$parentId) { - $parentId = $ensureFolder($parentRel); + $parentId = $ensureFolder($parentRel); $folders[$parentRel] = $parentId; } $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity; - $baseTitle = $rawTitle; + $baseTitle = $rawTitle; $finalTitle = $baseTitle; - $findExisting = function ($t) use ($docRepo, $parentRes, $courseEntity, $session, $group) { + $findExisting = function (string $t) use ($docRepo, $parentRes, $courseEntity, $session, $group) { $e = $docRepo->findCourseResourceByTitle($t, $parentRes->getResourceNode(), $courseEntity, $session, $group); - return $e && method_exists($e, 'getIid') ? $e->getIid() : null; }; @@ -715,21 +805,20 @@ public function restore_documents($session_id = 0, $respect_base_content = false $this->dlog('restore_documents: collision', ['title' => $finalTitle, 'policy' => $this->file_option]); if (FILE_SKIP === $this->file_option) { $this->course->resources[RESOURCE_DOCUMENT][$k]->destination_id = $existsIid; - continue; } - $pi = pathinfo($baseTitle); + $pi = pathinfo($baseTitle); $name = $pi['filename'] ?? $baseTitle; - $ext2 = isset($pi['extension']) && '' !== $pi['extension'] ? '.'.$pi['extension'] : ''; - $i = 1; + $ext2 = (isset($pi['extension']) && $pi['extension'] !== '') ? '.'.$pi['extension'] : ''; + $i = 1; while ($findExisting($finalTitle)) { $finalTitle = $name.'_'.$i.$ext2; $i++; } } - // Build content/realPath - $content = ''; + // Build content or set realPath for binary files + $content = ''; $realPath = ''; if ($isHtml) { @@ -738,11 +827,15 @@ public function restore_documents($session_id = 0, $respect_base_content = false $raw = utf8_encode($raw); } - // Rewrite using both maps (exact rel + basename fallback) BEFORE addDocument - $DBG('html:rewrite:before', ['title' => $finalTitle, 'byRel' => \count($urlMapByRel), 'byBase' => \count($urlMapByBase)]); + // Rewrite using maps (exact rel + basename fallback) BEFORE addDocument + $DBG('html:rewrite:before', [ + 'title' => $finalTitle, + 'byRel' => \count($urlMapByRel), + 'byBase' => \count($urlMapByBase), + ]); $rew = ChamiloHelper::rewriteLegacyCourseUrlsWithMap( $raw, - $courseDir, + ($courseInfo['directory'] ?? $courseInfo['code'] ?? ''), $urlMapByRel, $urlMapByBase ); @@ -776,10 +869,10 @@ public function restore_documents($session_id = 0, $respect_base_content = false $this->dlog('restore_documents: file created', [ 'title' => $finalTitle, - 'iid' => $iid, - 'mode' => $copyMode ? 'copy' : 'import', + 'iid' => $iid, + 'mode' => $copyMode ? 'copy' : 'import', ]); - } catch (Throwable $e) { + } catch (\Throwable $e) { $this->dlog('restore_documents: file create failed', ['title' => $finalTitle, 'error' => $e->getMessage()]); } } @@ -2681,8 +2774,8 @@ public function restore_quizzes($session_id = 0, $respect_base_content = false): ->setPropagateNeg((int) $quiz->propagate_neg) ->setHideQuestionTitle((bool) ($quiz->hide_question_title ?? false)) ->setHideQuestionNumber((int) ($quiz->hide_question_number ?? 0)) - ->setStartTime(!empty($quiz->start_time) ? new DateTime($quiz->start_time) : null) - ->setEndTime(!empty($quiz->end_time) ? new DateTime($quiz->end_time) : null) + ->setStartTime(!empty($quiz->start_time) ? new DateTime((string) $quiz->start_time) : null) + ->setEndTime(!empty($quiz->end_time) ? new DateTime((string) $quiz->end_time) : null) ; if (isset($quiz->access_condition) && '' !== $quiz->access_condition) { @@ -5858,4 +5951,19 @@ private function resetDoctrineIfClosed(): void error_log('COURSE_DEBUG: resetDoctrineIfClosed failed: '.$e->getMessage()); } } + + private function getCourseBackupsBase(): string + { + try { + if (method_exists(CourseArchiver::class, 'getBackupDir')) { + $dir = rtrim(CourseArchiver::getBackupDir(), '/'); + if ($dir !== '') { + return $dir; + } + } + } catch (\Throwable $e) { + } + + return rtrim(api_get_path(SYS_ARCHIVE_PATH), '/').'/course_backups'; + } }