diff --git a/src/CoreBundle/Tool/Maintenance.php b/src/CoreBundle/Tool/Maintenance.php
index e94bf560253..d4115b17693 100644
--- a/src/CoreBundle/Tool/Maintenance.php
+++ b/src/CoreBundle/Tool/Maintenance.php
@@ -20,7 +20,7 @@ public function getIcon(): string
public function getLink(): string
{
- return '/main/course_info/maintenance.php';
+ return '/resources/course_maintenance/:nodeId/';
}
public function getCategory(): string
diff --git a/src/CourseBundle/Component/CourseCopy/Course.php b/src/CourseBundle/Component/CourseCopy/Course.php
index 01754a93922..024724138d1 100644
--- a/src/CourseBundle/Component/CourseCopy/Course.php
+++ b/src/CourseBundle/Component/CourseCopy/Course.php
@@ -227,6 +227,20 @@ public function to_system_encoding()
case RESOURCE_FORUM:
case RESOURCE_QUIZ:
case RESOURCE_FORUMCATEGORY:
+ if (isset($resource->title)) {
+ $resource->title = api_to_system_encoding($resource->title, $this->encoding);
+ }
+ if (isset($resource->description)) {
+ $resource->description = api_to_system_encoding($resource->description, $this->encoding);
+ }
+ if (isset($resource->obj)) {
+ foreach (['cat_title','cat_comment','title','description'] as $f) {
+ if (isset($resource->obj->$f) && is_string($resource->obj->$f)) {
+ $resource->obj->$f = api_to_system_encoding($resource->obj->$f, $this->encoding);
+ }
+ }
+ }
+ break;
case RESOURCE_LINK:
case RESOURCE_LINKCATEGORY:
case RESOURCE_TEST_CATEGORY:
@@ -235,15 +249,15 @@ public function to_system_encoding()
break;
case RESOURCE_FORUMPOST:
- $resource->title = api_to_system_encoding($resource->title, $this->encoding);
- $resource->text = api_to_system_encoding($resource->text, $this->encoding);
- $resource->poster_name = api_to_system_encoding($resource->poster_name, $this->encoding);
-
+ if (isset($resource->title)) { $resource->title = api_to_system_encoding($resource->title, $this->encoding); }
+ if (isset($resource->text)) { $resource->text = api_to_system_encoding($resource->text, $this->encoding); }
+ if (isset($resource->poster_name)) { $resource->poster_name = api_to_system_encoding($resource->poster_name, $this->encoding); }
break;
+
case RESOURCE_FORUMTOPIC:
- $resource->title = api_to_system_encoding($resource->title, $this->encoding);
- $resource->topic_poster_name = api_to_system_encoding($resource->topic_poster_name, $this->encoding);
- $resource->title_qualify = api_to_system_encoding($resource->title_qualify, $this->encoding);
+ if (isset($resource->title)) { $resource->title = api_to_system_encoding($resource->title, $this->encoding); }
+ if (isset($resource->topic_poster_name)) { $resource->topic_poster_name = api_to_system_encoding($resource->topic_poster_name, $this->encoding); }
+ if (isset($resource->title_qualify)) { $resource->title_qualify = api_to_system_encoding($resource->title_qualify, $this->encoding); }
break;
case RESOURCE_GLOSSARY:
diff --git a/src/CourseBundle/Component/CourseCopy/CourseArchiver.php b/src/CourseBundle/Component/CourseCopy/CourseArchiver.php
index 20c48495530..f030315bc08 100644
--- a/src/CourseBundle/Component/CourseCopy/CourseArchiver.php
+++ b/src/CourseBundle/Component/CourseCopy/CourseArchiver.php
@@ -9,6 +9,7 @@
use DateTime;
use PclZip;
use Symfony\Component\Filesystem\Filesystem;
+use ZipArchive;
/**
* Some functions to write a course-object to a zip-file and to read a course-
@@ -270,16 +271,214 @@ public static function importUploadedFile($file)
}
/**
- * Read a course-object from a zip-file.
- *
- * @param string $filename
- * @param bool $delete Delete the file after reading the course?
- *
- * @return Course The course
+ * Read a legacy course backup (.zip) and return a Course object.
+ * - Extracts the zip into a temp dir.
+ * - Finds and decodes course_info.dat (base64 + serialize).
+ * - Registers legacy aliases/stubs BEFORE unserialize (critical).
+ * - Normalizes common identifier fields to int after unserialize.
+ */
+ public static function readCourse(string $filename, bool $delete = false): false|Course
+ {
+ // Clean temp backup dirs and ensure backup dir exists
+ self::cleanBackupDir();
+ self::createBackupDir();
+
+ $backupDir = rtrim(self::getBackupDir(), '/').'/';
+ $zipPath = $backupDir.$filename;
+
+ if (!is_file($zipPath)) {
+ throw new \RuntimeException('Backup file not found: '.$filename);
+ }
+
+ // 1) Extract zip into a temp directory
+ $tmp = $backupDir.'CourseArchiver_'.uniqid('', true).'/';
+ (new Filesystem())->mkdir($tmp);
+
+ $zip = new ZipArchive();
+ if (true !== $zip->open($zipPath)) {
+ throw new \RuntimeException('Cannot open zip: '.$filename);
+ }
+ if (!$zip->extractTo($tmp)) {
+ $zip->close();
+ throw new \RuntimeException('Cannot extract zip: '.$filename);
+ }
+ $zip->close();
+
+ // 2) Read and decode course_info.dat (base64 + serialize)
+ $courseInfoDat = $tmp.'course_info.dat';
+ if (!is_file($courseInfoDat)) {
+ // Fallback: search nested locations
+ $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tmp));
+ foreach ($rii as $f) {
+ if ($f->isFile() && $f->getFilename() === 'course_info.dat') {
+ $courseInfoDat = $f->getPathname();
+ break;
+ }
+ }
+ if (!is_file($courseInfoDat)) {
+ throw new \RuntimeException('course_info.dat not found in backup');
+ }
+ }
+
+ $raw = file_get_contents($courseInfoDat);
+ $payload = base64_decode($raw, true);
+ if ($payload === false) {
+ throw new \RuntimeException('course_info.dat is not valid base64');
+ }
+
+ // 3) Coerce numeric-string identifiers to integers *before* unserialize
+ // This prevents "Cannot assign string to property ... of type int"
+ // on typed properties (handles public/protected/private names).
+ $payload = self::coerceNumericStringsInSerialized($payload);
+
+ // 4) Register legacy aliases BEFORE unserialize (critical for v1 backups)
+ self::registerLegacyAliases();
+
+ // 5) Unserialize using UnserializeApi if present (v1-compatible)
+ if (class_exists('UnserializeApi')) {
+ /** @var Course $course */
+ $course = \UnserializeApi::unserialize('course', $payload);
+ } else {
+ /** @var Course|false $course */
+ $course = @unserialize($payload, ['allowed_classes' => true]);
+ }
+
+ if (!is_object($course)) {
+ throw new \RuntimeException('Could not unserialize legacy course');
+ }
+
+ // 6) Normalize common numeric identifiers after unserialize (extra safety)
+ self::normalizeIds($course);
+
+ // 7) Optionally delete uploaded file (compat with v1)
+ if ($delete && is_file($zipPath)) {
+ @unlink($zipPath);
+ }
+
+ // Keep temp dir until restore phase if files are needed later (compat with v1)
+ return $course;
+ }
+
+ /**
+ * Convert selected numeric-string fields to integers inside the serialized payload
+ * to avoid "Cannot assign string to property ... of type int" on typed properties.
*
- * @todo Check if the archive is a correct Chamilo-export
+ * It handles public, protected ("\0*\0key") and private ("\0Class\0key") property names.
+ * We only coerce known identifier keys to keep it safe.
*/
- public static function readCourse($filename, $delete = false)
+ private static function coerceNumericStringsInSerialized(string $ser): string
+ {
+ // Identifier keys that must be integers
+ $keys = [
+ 'id','iid','c_id','parent_id','thematic_id','attendance_id',
+ 'room_id','display_order','session_id','category_id',
+ ];
+
+ /**
+ * Build a pattern that matches any of these name encodings:
+ * - public: "id"
+ * - protected:"\0*\0id"
+ * - private: "\0SomeClass\0id"
+ *
+ * We don't touch the key itself (so its s:N length stays valid).
+ * We only replace the *value* part: s:M:"123" => i:123
+ */
+ $alternatives = [];
+ foreach ($keys as $k) {
+ // public
+ $alternatives[] = preg_quote($k, '/');
+ // protected
+ $alternatives[] = "\x00\*\x00".preg_quote($k, '/');
+ // private (class-specific; we accept any class name between NULs)
+ $alternatives[] = "\x00[^\x00]+\x00".preg_quote($k, '/');
+ }
+ $nameAlt = '(?:'.implode('|', $alternatives).')';
+
+ // Full pattern:
+ // (s:\d+:"
";) s:\d+:"()";
+ // Note: we *must not* use /u because of NUL bytes; keep binary-safe regex.
+ $pattern = '/(s:\d+:"'.$nameAlt.'";)s:\d+:"(\d+)";/s';
+
+ return preg_replace_callback(
+ $pattern,
+ static fn($m) => $m[1].'i:'.$m[2].';',
+ $ser
+ );
+ }
+
+
+ /**
+ * Recursively cast common identifier fields to int after unserialize.
+ * Safe to call on arrays/objects/stdClass/legacy resource objects.
+ */
+ private static function normalizeIds(mixed &$node): void
+ {
+ $castKeys = [
+ 'id','iid','c_id','parent_id','thematic_id','attendance_id',
+ 'room_id','display_order','session_id','category_id',
+ ];
+
+ if (is_array($node)) {
+ foreach ($node as $k => &$v) {
+ if (is_string($k) && in_array($k, $castKeys, true) && (is_string($v) || is_numeric($v))) {
+ $v = (int) $v;
+ } else {
+ self::normalizeIds($v);
+ }
+ }
+ return;
+ }
+
+ if (is_object($node)) {
+ foreach (get_object_vars($node) as $k => $v) {
+ if (in_array($k, $castKeys, true) && (is_string($v) || is_numeric($v))) {
+ $node->$k = (int) $v;
+ continue;
+ }
+ self::normalizeIds($node->$k);
+ }
+ }
+ }
+
+
+ /** Keep the old alias map so unserialize works exactly like v1 */
+ private static function registerLegacyAliases(): void
{
+ $aliases = [
+ 'Chamilo\CourseBundle\Component\CourseCopy\Course' => 'Course',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Announcement' => 'Announcement',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance' => 'Attendance',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent'=> 'CalendarEvent',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyLearnpath' => 'CourseCopyLearnpath',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyTestCategory' => 'CourseCopyTestCategory',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription' => 'CourseDescription',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseSession' => 'CourseSession',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Document' => 'Document',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Forum' => 'Forum',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumCategory'=> 'ForumCategory',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumPost' => 'ForumPost',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumTopic' => 'ForumTopic',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary' => 'Glossary',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup' => 'GradeBookBackup',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Link' => 'Link',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\LinkCategory' => 'LinkCategory',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Quiz' => 'Quiz',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestion' => 'QuizQuestion',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestionOption' => 'QuizQuestionOption',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\ScormDocument'=> 'ScormDocument',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Survey' => 'Survey',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyInvitation' => 'SurveyInvitation',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyQuestion'=> 'SurveyQuestion',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic' => 'Thematic',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\ToolIntro' => 'ToolIntro',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki' => 'Wiki',
+ 'Chamilo\CourseBundle\Component\CourseCopy\Resources\Work' => 'Work',
+ ];
+
+ foreach ($aliases as $fqcn => $alias) {
+ if (!class_exists($alias)) {
+ class_alias($fqcn, $alias);
+ }
+ }
}
}
diff --git a/src/CourseBundle/Component/CourseCopy/CourseBuilder.php b/src/CourseBundle/Component/CourseCopy/CourseBuilder.php
index f80dcfc9bbe..f9d5ef8edff 100644
--- a/src/CourseBundle/Component/CourseCopy/CourseBuilder.php
+++ b/src/CourseBundle/Component/CourseCopy/CourseBuilder.php
@@ -1,319 +1,448 @@
+ * CourseBuilder focused on Doctrine/ResourceNode export (keeps legacy orchestration).
*/
class CourseBuilder
{
- /** @var Course */
+ /** @var Course Legacy course container used by the exporter */
public $course;
- /* With this array you can filter the tools you want to be parsed by
- default all tools are included */
- public $tools_to_build = [
- 'announcements',
- 'attendance',
- 'course_descriptions',
- 'documents',
- 'events',
- 'forum_category',
- 'forums',
- 'forum_topics',
- 'glossary',
- 'quizzes',
- 'test_category',
- 'learnpath_category',
- 'learnpaths',
- 'links',
- 'surveys',
- 'tool_intro',
- 'thematic',
- 'wiki',
- 'works',
- 'gradebook',
+ /** @var array Only the tools to build (defaults kept) */
+ public array $tools_to_build = [
+ 'documents', 'forums', 'tool_intro', 'links', 'quizzes', 'quiz_questions',
+ 'assets', 'surveys', 'survey_questions', 'announcements', 'events',
+ 'course_descriptions', 'glossary', 'wiki', 'thematic', 'attendance', 'works',
+ 'gradebook', 'learnpath_category', 'learnpaths',
];
- public $toolToName = [
- 'announcements' => RESOURCE_ANNOUNCEMENT,
- 'attendance' => RESOURCE_ATTENDANCE,
- 'course_descriptions' => RESOURCE_COURSEDESCRIPTION,
- 'documents' => RESOURCE_DOCUMENT,
- 'events' => RESOURCE_EVENT,
- 'forum_category' => RESOURCE_FORUMCATEGORY,
- 'forums' => RESOURCE_FORUM,
- 'forum_topics' => RESOURCE_FORUMTOPIC,
- 'glossary' => RESOURCE_GLOSSARY,
- 'quizzes' => RESOURCE_QUIZ,
- 'test_category' => RESOURCE_TEST_CATEGORY,
+ /** @var array Legacy constant map (extend as you add tools) */
+ public array $toolToName = [
+ 'documents' => RESOURCE_DOCUMENT,
+ 'forums' => RESOURCE_FORUM,
+ 'tool_intro' => RESOURCE_TOOL_INTRO,
+ 'links' => RESOURCE_LINK,
+ 'quizzes' => RESOURCE_QUIZ,
+ 'quiz_questions' => RESOURCE_QUIZQUESTION,
+ 'assets' => 'asset',
+ 'surveys' => RESOURCE_SURVEY,
+ 'survey_questions' => RESOURCE_SURVEYQUESTION,
+ 'announcements' => RESOURCE_ANNOUNCEMENT,
+ 'events' => RESOURCE_EVENT,
+ 'course_descriptions'=> RESOURCE_COURSEDESCRIPTION,
+ 'glossary' => RESOURCE_GLOSSARY,
+ 'wiki' => RESOURCE_WIKI,
+ 'thematic' => RESOURCE_THEMATIC,
+ 'attendance' => RESOURCE_ATTENDANCE,
+ 'works' => RESOURCE_WORK,
+ 'gradebook' => RESOURCE_GRADEBOOK,
+ 'learnpaths' => RESOURCE_LEARNPATH,
'learnpath_category' => RESOURCE_LEARNPATH_CATEGORY,
- 'learnpaths' => RESOURCE_LEARNPATH,
- 'links' => RESOURCE_LINK,
- 'surveys' => RESOURCE_SURVEY,
- 'tool_intro' => RESOURCE_TOOL_INTRO,
- 'thematic' => RESOURCE_THEMATIC,
- 'wiki' => RESOURCE_WIKI,
- 'works' => RESOURCE_WORK,
- 'gradebook' => RESOURCE_GRADEBOOK,
];
- /* With this array you can filter wich elements of the tools are going
- to be added in the course obj (only works with LPs) */
- public $specific_id_list = [];
- public $documentsAddedInText = [];
+ /** @var array> Optional whitelist of IDs per tool */
+ public array $specific_id_list = [];
+
+ /** @var array Documents referenced inside HTML */
+ public array $documentsAddedInText = [];
+
+ /** Doctrine services */
+ private $em = null; // Doctrine EntityManager
+ private $docRepo = null; // CDocumentRepository
/**
- * Create a new CourseBuilder.
+ * Constructor (keeps legacy init; wires Doctrine repositories).
*
- * @param string $type
- * @param null $course
+ * @param string $type 'partial'|'complete'
+ * @param array|null $course Optional course info array
*/
public function __construct($type = '', $course = null)
{
+ // Legacy behavior preserved
$_course = api_get_course_info();
-
if (!empty($course['official_code'])) {
$_course = $course;
}
- $this->course = new Course();
- $this->course->code = $_course['code'];
- $this->course->type = $type;
- // $this->course->path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/';
- // $this->course->backup_path = api_get_path(SYS_COURSE_PATH).$_course['path'];
+ $this->course = new Course();
+ $this->course->code = $_course['code'];
+ $this->course->type = $type;
$this->course->encoding = api_get_system_encoding();
- $this->course->info = $_course;
+ $this->course->info = $_course;
+
+ $this->em = Database::getManager();
+ $this->docRepo = Container::getDocumentRepository();
+
+ // Use $this->em / $this->docRepo in build_documents() when needed.
}
/**
- * @param array $list
+ * Merge a parsed list of document refs into memory.
+ *
+ * @param array $list
*/
- public function addDocumentList($list)
+ public function addDocumentList(array $list): void
{
foreach ($list as $item) {
- if (!in_array($item[0], $this->documentsAddedInText)) {
+ if (!in_array($item[0], $this->documentsAddedInText, true)) {
$this->documentsAddedInText[$item[0]] = $item;
}
}
}
/**
- * @param string $text
+ * Parse HTML and collect referenced course documents.
+ *
+ * @param string $html HTML content
*/
- public function findAndSetDocumentsInText($text)
+ public function findAndSetDocumentsInText(string $html = ''): void
{
- $documentList = DocumentManager::get_resources_from_source_html($text);
+ if ($html === '') {
+ return;
+ }
+ $documentList = \DocumentManager::get_resources_from_source_html($html);
$this->addDocumentList($documentList);
}
/**
- * Parse documents added in the documentsAddedInText variable.
+ * Resolve collected HTML links to CDocument iids via the ResourceNode tree and build them.
+ *
+ * @return void
*/
- public function restoreDocumentsFromList()
+ public function restoreDocumentsFromList(): void
{
- if (!empty($this->documentsAddedInText)) {
- $list = [];
- $courseInfo = api_get_course_info();
- foreach ($this->documentsAddedInText as $item) {
- // Get information about source url
- $url = $item[0]; // url
- $scope = $item[1]; // scope (local, remote)
- $type = $item[2]; // type (rel, abs, url)
-
- $origParseUrl = parse_url($url);
- $realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
-
- if ('local' == $scope) {
- if ('abs' == $type || 'rel' == $type) {
- $documentFile = strstr($realOrigPath, 'document');
- if (false !== strpos($realOrigPath, $documentFile)) {
- $documentFile = str_replace('document', '', $documentFile);
- $itemDocumentId = DocumentManager::get_document_id($courseInfo, $documentFile);
- // Document found! Add it to the list
- if ($itemDocumentId) {
- $list[] = $itemDocumentId;
- }
- }
- }
- }
+ if (empty($this->documentsAddedInText)) {
+ return;
+ }
+
+ $courseInfo = api_get_course_info();
+ $courseCode = (string) ($courseInfo['code'] ?? '');
+ if ($courseCode === '') {
+ return;
+ }
+
+ /** @var CourseEntity|null $course */
+ $course = $this->em->getRepository(CourseEntity::class)->findOneBy(['code' => $courseCode]);
+ if (!$course instanceof CourseEntity) {
+ return;
+ }
+
+ // Documents root under the course
+ $root = $this->docRepo->getCourseDocumentsRootNode($course);
+ if (!$root instanceof ResourceNode) {
+ return;
+ }
+
+ $iids = [];
+
+ foreach ($this->documentsAddedInText as $item) {
+ [$url, $scope, $type] = $item; // url, scope(local/remote), type(rel/abs/url)
+ if ($scope !== 'local' || !\in_array($type, ['rel', 'abs'], true)) {
+ continue;
+ }
+
+ $segments = $this->extractDocumentSegmentsFromUrl((string) $url);
+ if (!$segments) {
+ continue;
}
+ // Walk the ResourceNode tree by matching child titles
+ $node = $this->resolveNodeBySegments($root, $segments);
+ if (!$node) {
+ continue;
+ }
+
+ $resource = $this->docRepo->getResourceByResourceNode($node);
+ if ($resource instanceof CDocument && is_int($resource->getIid())) {
+ $iids[] = $resource->getIid();
+ }
+ }
+
+ $iids = array_values(array_unique($iids));
+ if ($iids) {
$this->build_documents(
api_get_session_id(),
- api_get_course_int_id(),
+ (int) $course->getId(),
true,
- $list
+ $iids
);
}
}
/**
- * @param array $array
+ * Extract path segments after "/document".
+ *
+ * @param string $url
+ * @return array
*/
- public function set_tools_to_build($array)
+ private function extractDocumentSegmentsFromUrl(string $url): array
+ {
+ $decoded = urldecode($url);
+ if (!preg_match('#/document(/.*)$#', $decoded, $m)) {
+ return [];
+ }
+ $tail = trim($m[1], '/'); // e.g. "Folder/Sub/file.pdf"
+ if ($tail === '') {
+ return [];
+ }
+
+ $parts = array_values(array_filter(explode('/', $tail), static fn($s) => $s !== ''));
+ return array_map(static fn($s) => trim($s), $parts);
+ }
+
+ /**
+ * Walk children by title from a given parent node.
+ *
+ * @param ResourceNode $parent
+ * @param array $segments
+ * @return ResourceNode|null
+ */
+ private function resolveNodeBySegments(ResourceNode $parent, array $segments): ?ResourceNode
+ {
+ $node = $parent;
+ foreach ($segments as $title) {
+ $child = $this->docRepo->findChildNodeByTitle($node, $title);
+ if (!$child instanceof ResourceNode) {
+ return null;
+ }
+ $node = $child;
+ }
+ return $node;
+ }
+
+ /**
+ * Set tools to build.
+ *
+ * @param array $array
+ */
+ public function set_tools_to_build(array $array): void
{
$this->tools_to_build = $array;
}
/**
- * @param array $array
+ * Set specific id list per tool.
+ *
+ * @param array> $array
*/
- public function set_tools_specific_id_list($array)
+ public function set_tools_specific_id_list(array $array): void
{
$this->specific_id_list = $array;
}
/**
- * Get the created course.
+ * Get legacy Course container.
*
- * @return course The course
+ * @return Course
*/
- public function get_course()
+ public function get_course(): Course
{
return $this->course;
}
/**
- * Build the course-object.
- *
- * @param int $session_id
- * @param string $courseCode
- * @param bool $withBaseContent true if you want to get the elements that exists in the course and
- * in the session, (session_id = 0 or session_id = X)
- * @param array $parseOnlyToolList
- * @param array $toolsFromPost
+ * Build the course (documents already repo-based; other tools preserved).
*
- * @return Course The course object structure
+ * @param int $session_id
+ * @param string $courseCode
+ * @param bool $withBaseContent
+ * @param array $parseOnlyToolList
+ * @param array $toolsFromPost
+ * @return Course
*/
public function build(
- $session_id = 0,
- $courseCode = '',
- $withBaseContent = false,
- $parseOnlyToolList = [],
- $toolsFromPost = []
- ) {
- $course = api_get_course_info($courseCode);
- $courseId = $course['real_id'];
- foreach ($this->tools_to_build as $tool) {
- if (!empty($parseOnlyToolList) && !in_array($this->toolToName[$tool], $parseOnlyToolList)) {
- continue;
- }
- $function_build = 'build_'.$tool;
- $specificIdList = isset($this->specific_id_list[$tool]) ? $this->specific_id_list[$tool] : null;
- $buildOrphanQuestions = true;
- if ('quizzes' === $tool) {
- if (!isset($toolsFromPost[RESOURCE_QUIZ][-1])) {
- $buildOrphanQuestions = false;
+ int $session_id = 0,
+ string $courseCode = '',
+ bool $withBaseContent = false,
+ array $parseOnlyToolList = [],
+ array $toolsFromPost = []
+ ): Course {
+ /** @var CourseEntity|null $courseEntity */
+ $courseEntity = $courseCode !== ''
+ ? $this->em->getRepository(CourseEntity::class)->findOneBy(['code' => $courseCode])
+ : $this->em->getRepository(CourseEntity::class)->find(api_get_course_int_id());
+
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $session_id
+ ? $this->em->getRepository(SessionEntity::class)->find($session_id)
+ : null;
+
+ // Legacy DTO where resources[...] are built
+ $legacyCourse = $this->course;
+
+ foreach ($this->tools_to_build as $toolKey) {
+ if (!empty($parseOnlyToolList)) {
+ $const = $this->toolToName[$toolKey] ?? null;
+ if ($const !== null && !in_array($const, $parseOnlyToolList, true)) {
+ continue;
}
+ }
- // Force orphan load
- if ('complete' === $this->course->type) {
- $buildOrphanQuestions = true;
- }
+ if ($toolKey === 'documents') {
+ $ids = $this->specific_id_list['documents'] ?? [];
+ $this->build_documents_with_repo($courseEntity, $sessionEntity, $withBaseContent, $ids);
+ }
+
+ if ($toolKey === 'forums' || $toolKey === 'forum') {
+ $ids = $this->specific_id_list['forums'] ?? $this->specific_id_list['forum'] ?? [];
+ $this->build_forum_category($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ $this->build_forums($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ $this->build_forum_topics($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ $this->build_forum_posts($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'tool_intro') {
+ $this->build_tool_intro($legacyCourse, $courseEntity, $sessionEntity);
+ }
- $this->build_quizzes(
- $session_id,
- $courseId,
- $withBaseContent,
- $specificIdList,
- $buildOrphanQuestions
+ if ($toolKey === 'links') {
+ $ids = $this->specific_id_list['links'] ?? [];
+ $this->build_links($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'quizzes' || $toolKey === 'quiz') {
+ $ids = $this->specific_id_list['quizzes'] ?? $this->specific_id_list['quiz'] ?? [];
+ $neededQuestionIds = $this->build_quizzes($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ // Always export question bucket required by the quizzes
+ $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $neededQuestionIds);
+ error_log(
+ 'COURSE_BUILD: quizzes='.count($legacyCourse->resources[RESOURCE_QUIZ] ?? []).
+ ' quiz_questions='.count($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? [])
);
- } else {
- $this->$function_build(
- $session_id,
- $courseId,
- $withBaseContent,
- $specificIdList
+ }
+
+ if ($toolKey === 'quiz_questions') {
+ $ids = $this->specific_id_list['quiz_questions'] ?? [];
+ $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $ids);
+ error_log(
+ 'COURSE_BUILD: explicit quiz_questions='.count($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? [])
);
}
- }
- // Add asset
- /*if ($course['course_image_source'] && basename($course['course_image_source']) !== 'course.png') {
- // Add course image courses/XXX/course-pic85x85.png
- $asset = new Asset(
- $course['course_image_source'],
- basename($course['course_image_source']),
- basename($course['course_image_source'])
- );
- $this->course->add_resource($asset);
+ if ($toolKey === 'surveys' || $toolKey === 'survey') {
+ $ids = $this->specific_id_list['surveys'] ?? $this->specific_id_list['survey'] ?? [];
+ $neededQ = $this->build_surveys($this->course, $courseEntity, $sessionEntity, $ids);
+ $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, $neededQ);
+ }
- $asset = new Asset(
- $course['course_image_large_source'],
- basename($course['course_image_large_source']),
- basename($course['course_image_large_source'])
- );
- $this->course->add_resource($asset);
- }*/
-
- // Once we've built the resources array a bit more, try to get items
- // from the item_property table and order them in the "resources" array
- $table = Database::get_course_table(TABLE_ITEM_PROPERTY);
- foreach ($this->course->resources as $type => $resources) {
- if (!empty($parseOnlyToolList) && !in_array($this->toolToName[$tool], $parseOnlyToolList)) {
- continue;
+ if ($toolKey === 'survey_questions') {
+ $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, []);
}
- foreach ($resources as $id => $resource) {
- if ($resource) {
- $tool = $resource->get_tool();
- if (null != $tool) {
- $sql = "SELECT * FROM $table
- WHERE
- c_id = $courseId AND
- tool = '".$tool."' AND
- ref = '".$resource->get_id()."'";
- $res = Database::query($sql);
- $properties = [];
- while ($property = Database::fetch_array($res)) {
- $properties[] = $property;
- }
- $this->course->resources[$type][$id]->item_properties = $properties;
- }
- }
+
+ if ($toolKey === 'announcements') {
+ $ids = $this->specific_id_list['announcements'] ?? [];
+ $this->build_announcements($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'events') {
+ $ids = $this->specific_id_list['events'] ?? [];
+ $this->build_events($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'course_descriptions') {
+ $ids = $this->specific_id_list['course_descriptions'] ?? [];
+ $this->build_course_descriptions($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'glossary') {
+ $ids = $this->specific_id_list['glossary'] ?? [];
+ $this->build_glossary($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'wiki') {
+ $ids = $this->specific_id_list['wiki'] ?? [];
+ $this->build_wiki($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'thematic') {
+ $ids = $this->specific_id_list['thematic'] ?? [];
+ $this->build_thematic($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'attendance') {
+ $ids = $this->specific_id_list['attendance'] ?? [];
+ $this->build_attendance($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'works') {
+ $ids = $this->specific_id_list['works'] ?? [];
+ $this->build_works($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'gradebook') {
+ $this->build_gradebook($this->course, $courseEntity, $sessionEntity);
+ }
+
+ if ($toolKey === 'learnpath_category') {
+ $ids = $this->specific_id_list['learnpath_category'] ?? [];
+ $this->build_learnpath_category($this->course, $courseEntity, $sessionEntity, $ids);
+ }
+
+ if ($toolKey === 'learnpaths') {
+ $ids = $this->specific_id_list['learnpaths'] ?? [];
+ $this->build_learnpaths($this->course, $courseEntity, $sessionEntity, $ids, true);
}
}
@@ -321,1586 +450,1860 @@ public function build(
}
/**
- * Build the documents.
+ * Export Learnpath categories (CLpCategory).
*
- * @param int $session_id
- * @param int $courseId
- * @param bool $withBaseContent
- * @param array $idList
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_documents(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $table_doc = Database::get_course_table(TABLE_DOCUMENT);
- $table_prop = Database::get_course_table(TABLE_ITEM_PROPERTY);
-
- // Remove chat_files and shared_folder files
- $avoid_paths = "
- path NOT LIKE '/shared_folder%' AND
- path NOT LIKE '/chat_files%' AND
- path NOT LIKE '/../exercises/%'
- ";
- $documentCondition = '';
- if (!empty($idList)) {
- $idList = array_unique($idList);
- $idList = array_map('intval', $idList);
- $documentCondition = ' d.iid IN ("'.implode('","', $idList).'") AND ';
- }
-
- if (!empty($courseId) && !empty($session_id)) {
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $session_condition = api_get_session_condition(
- $session_id,
- true,
- true,
- 'd.session_id'
- );
- } else {
- $session_condition = api_get_session_condition(
- $session_id,
- true,
- false,
- 'd.session_id'
- );
- }
+ private function build_learnpath_category(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
- if (!empty($this->course->type) && 'partial' == $this->course->type) {
- $sql = "SELECT d.iid, d.path, d.comment, d.title, d.filetype, d.size
- FROM $table_doc d
- INNER JOIN $table_prop p
- ON (p.ref = d.id AND d.c_id = p.c_id)
- WHERE
- d.c_id = $courseId AND
- p.c_id = $courseId AND
- tool = '".TOOL_DOCUMENT."' AND
- $documentCondition
- p.visibility != 2 AND
- path NOT LIKE '/images/gallery%' AND
- $avoid_paths
- $session_condition
- ORDER BY path";
- } else {
- $sql = "SELECT d.iid, d.path, d.comment, d.title, d.filetype, d.size
- FROM $table_doc d
- INNER JOIN $table_prop p
- ON (p.ref = d.id AND d.c_id = p.c_id)
- WHERE
- d.c_id = $courseId AND
- p.c_id = $courseId AND
- tool = '".TOOL_DOCUMENT."' AND
- $documentCondition
- $avoid_paths AND
- p.visibility != 2 $session_condition
- ORDER BY path";
- }
+ $repo = Container::getLpCategoryRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $doc = new Document(
- $obj->iid,
- $obj->path,
- $obj->comment,
- $obj->title,
- $obj->filetype,
- $obj->size
- );
- $this->course->add_resource($doc);
- }
- } else {
- if (!empty($this->course->type) && 'partial' == $this->course->type) {
- $sql = "SELECT d.iid, d.path, d.comment, d.title, d.filetype, d.size
- FROM $table_doc d
- INNER JOIN $table_prop p
- ON (p.ref = d.id AND d.c_id = p.c_id)
- WHERE
- d.c_id = $courseId AND
- p.c_id = $courseId AND
- tool = '".TOOL_DOCUMENT."' AND
- $documentCondition
- p.visibility != 2 AND
- path NOT LIKE '/images/gallery%' AND
- $avoid_paths AND
- (d.session_id = 0 OR d.session_id IS NULL)
- ORDER BY path";
- } else {
- $sql = "SELECT d.iid, d.path, d.comment, d.title, d.filetype, d.size
- FROM $table_doc d
- INNER JOIN $table_prop p
- ON (p.ref = d.id AND d.c_id = p.c_id)
- WHERE
- d.c_id = $courseId AND
- p.c_id = $courseId AND
- tool = '".TOOL_DOCUMENT."' AND
- $documentCondition
- p.visibility != 2 AND
- $avoid_paths AND
- (d.session_id = 0 OR d.session_id IS NULL)
- ORDER BY path";
- }
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
- $result = Database::query($sql);
- while ($obj = Database::fetch_object($result)) {
- $doc = new Document(
- $obj->iid,
- $obj->path,
- $obj->comment,
- $obj->title,
- $obj->filetype,
- $obj->size
- );
- $this->course->add_resource($doc);
- }
+ /** @var CLpCategory[] $rows */
+ $rows = $qb->getQuery()->getResult();
+
+ foreach ($rows as $cat) {
+ $iid = (int) $cat->getIid();
+ $title = (string) $cat->getTitle();
+
+ $payload = [
+ 'id' => $iid,
+ 'title' => $title,
+ ];
+
+ $legacyCourse->resources[RESOURCE_LEARNPATH_CATEGORY][$iid] =
+ $this->mkLegacyItem(RESOURCE_LEARNPATH_CATEGORY, $iid, $payload);
}
}
/**
- * Build the forums.
+ * Export Learnpaths (CLp) + items, with optional SCORM folder packing.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $idList
+ * @param bool $addScormFolder
+ * @return void
*/
- public function build_forums(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $table = Database::get_course_table(TABLE_FORUM);
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function build_learnpaths(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $idList = [],
+ bool $addScormFolder = true
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
+
+ $lpRepo = Container::getLpRepository();
+ $qb = $lpRepo->getResourcesByCourse($courseEntity, $sessionEntity);
- $idCondition = '';
if (!empty($idList)) {
- $idList = array_unique($idList);
- $idList = array_map('intval', $idList);
- $idCondition = ' AND iid IN ("'.implode('","', $idList).'") ';
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))));
}
- $sql = "SELECT * FROM $table WHERE c_id = $courseId $sessionCondition $idCondition";
- $sql .= ' ORDER BY forum_title, forum_category';
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $forum = new Forum($obj);
- $this->course->add_resource($forum);
+ /** @var CLp[] $lps */
+ $lps = $qb->getQuery()->getResult();
+
+ foreach ($lps as $lp) {
+ $iid = (int) $lp->getIid();
+ $lpType = (int) $lp->getLpType(); // 1=LP, 2=SCORM, 3=AICC
+
+ $items = [];
+ /** @var CLpItem $it */
+ foreach ($lp->getItems() as $it) {
+ $items[] = [
+ 'id' => (int) $it->getIid(),
+ 'item_type' => (string) $it->getItemType(),
+ 'ref' => (string) $it->getRef(),
+ 'title' => (string) $it->getTitle(),
+ 'name' => (string) $lp->getTitle(),
+ 'description' => (string) ($it->getDescription() ?? ''),
+ 'path' => (string) $it->getPath(),
+ 'min_score' => (float) $it->getMinScore(),
+ 'max_score' => $it->getMaxScore() !== null ? (float) $it->getMaxScore() : null,
+ 'mastery_score' => $it->getMasteryScore() !== null ? (float) $it->getMasteryScore() : null,
+ 'parent_item_id' => (int) $it->getParentItemId(),
+ 'previous_item_id' => $it->getPreviousItemId() !== null ? (int) $it->getPreviousItemId() : null,
+ 'next_item_id' => $it->getNextItemId() !== null ? (int) $it->getNextItemId() : null,
+ 'display_order' => (int) $it->getDisplayOrder(),
+ 'prerequisite' => (string) ($it->getPrerequisite() ?? ''),
+ 'parameters' => (string) ($it->getParameters() ?? ''),
+ 'launch_data' => (string) $it->getLaunchData(),
+ 'audio' => (string) ($it->getAudio() ?? ''),
+ ];
+ }
+
+ $payload = [
+ 'id' => $iid,
+ 'lp_type' => $lpType,
+ 'title' => (string) $lp->getTitle(),
+ 'path' => (string) $lp->getPath(),
+ 'ref' => (string) ($lp->getRef() ?? ''),
+ 'description' => (string) ($lp->getDescription() ?? ''),
+ 'content_local' => (string) $lp->getContentLocal(),
+ 'default_encoding' => (string) $lp->getDefaultEncoding(),
+ 'default_view_mod' => (string) $lp->getDefaultViewMod(),
+ 'prevent_reinit' => (bool) $lp->getPreventReinit(),
+ 'force_commit' => (bool) $lp->getForceCommit(),
+ 'content_maker' => (string) $lp->getContentMaker(),
+ 'display_order' => (int) $lp->getDisplayNotAllowedLp(),
+ 'js_lib' => (string) $lp->getJsLib(),
+ 'content_license' => (string) $lp->getContentLicense(),
+ 'debug' => (bool) $lp->getDebug(),
+ 'visibility' => '1',
+ 'author' => (string) $lp->getAuthor(),
+ 'use_max_score' => (int) $lp->getUseMaxScore(),
+ 'autolaunch' => (int) $lp->getAutolaunch(),
+ 'created_on' => $this->fmtDate($lp->getCreatedOn()),
+ 'modified_on' => $this->fmtDate($lp->getModifiedOn()),
+ 'published_on' => $this->fmtDate($lp->getPublishedOn()),
+ 'expired_on' => $this->fmtDate($lp->getExpiredOn()),
+ 'session_id' => (int) ($sessionEntity?->getId() ?? 0),
+ 'category_id' => (int) ($lp->getCategory()?->getIid() ?? 0),
+ 'items' => $items,
+ ];
+
+ $legacyCourse->resources[RESOURCE_LEARNPATH][$iid] =
+ $this->mkLegacyItem(RESOURCE_LEARNPATH, $iid, $payload, ['items']);
+ }
+
+ // Optional: pack “scorm” folder (legacy parity)
+ if ($addScormFolder && isset($this->course->backup_path)) {
+ $scormDir = rtrim((string) $this->course->backup_path, '/') . '/scorm';
+ if (is_dir($scormDir) && ($dh = @opendir($scormDir))) {
+ $i = 1;
+ while (false !== ($file = readdir($dh))) {
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+ if (is_dir($scormDir . '/' . $file)) {
+ $payload = ['path' => '/' . $file, 'name' => (string) $file];
+ $legacyCourse->resources['scorm'][$i] =
+ $this->mkLegacyItem('scorm', $i, $payload);
+ $i++;
+ }
+ }
+ closedir($dh);
+ }
}
}
/**
- * Build a forum-category.
+ * Export Gradebook (categories + evaluations + links).
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @return void
*/
- public function build_forum_category(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $table = Database::get_course_table(TABLE_FORUM_CATEGORY);
-
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function build_gradebook(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
- $idCondition = '';
- if (!empty($idList)) {
- $idList = array_unique($idList);
- $idList = array_map('intval', $idList);
- $idCondition = ' AND iid IN ("'.implode('","', $idList).'") ';
+ /** @var EntityManagerInterface $em */
+ $em = \Database::getManager();
+ $catRepo = $em->getRepository(GradebookCategory::class);
+
+ $criteria = ['course' => $courseEntity];
+ if ($sessionEntity) {
+ $criteria['session'] = $sessionEntity;
}
- $sql = "SELECT * FROM $table
- WHERE c_id = $courseId $sessionCondition $idCondition
- ORDER BY title";
+ /** @var GradebookCategory[] $cats */
+ $cats = $catRepo->findBy($criteria);
+ if (!$cats) {
+ return;
+ }
- $result = Database::query($sql);
- while ($obj = Database::fetch_object($result)) {
- $forumCategory = new ForumCategory($obj);
- $this->course->add_resource($forumCategory);
+ $payloadCategories = [];
+ foreach ($cats as $cat) {
+ $payloadCategories[] = $this->serializeGradebookCategory($cat);
}
+
+ $backup = new GradeBookBackup($payloadCategories);
+ $legacyCourse->add_resource($backup);
}
/**
- * Build the forum-topics.
+ * Serialize GradebookCategory (and nested parts) to array for restore.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
+ * @param GradebookCategory $c
+ * @return array
*/
- public function build_forum_topics(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $table = Database::get_course_table(TABLE_FORUM_THREAD);
-
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function serializeGradebookCategory(GradebookCategory $c): array
+ {
+ $arr = [
+ 'id' => (int) $c->getId(),
+ 'title' => (string) $c->getTitle(),
+ 'description' => (string) ($c->getDescription() ?? ''),
+ 'weight' => (float) $c->getWeight(),
+ 'visible' => (bool) $c->getVisible(),
+ 'locked' => (int) $c->getLocked(),
+ 'parent_id' => $c->getParent() ? (int) $c->getParent()->getId() : 0,
+ 'generate_certificates' => (bool) $c->getGenerateCertificates(),
+ 'certificate_validity_period'=> $c->getCertificateValidityPeriod(),
+ 'is_requirement' => (bool) $c->getIsRequirement(),
+ 'default_lowest_eval_exclude'=> (bool) $c->getDefaultLowestEvalExclude(),
+ 'minimum_to_validate' => $c->getMinimumToValidate(),
+ 'gradebooks_to_validate_in_dependence' => $c->getGradeBooksToValidateInDependence(),
+ 'allow_skills_by_subcategory'=> $c->getAllowSkillsBySubcategory(),
+ // camelCase duplicates (future-proof)
+ 'generateCertificates' => (bool) $c->getGenerateCertificates(),
+ 'certificateValidityPeriod' => $c->getCertificateValidityPeriod(),
+ 'isRequirement' => (bool) $c->getIsRequirement(),
+ 'defaultLowestEvalExclude' => (bool) $c->getDefaultLowestEvalExclude(),
+ 'minimumToValidate' => $c->getMinimumToValidate(),
+ 'gradeBooksToValidateInDependence' => $c->getGradeBooksToValidateInDependence(),
+ 'allowSkillsBySubcategory' => $c->getAllowSkillsBySubcategory(),
+ ];
- $idCondition = '';
- if (!empty($idList)) {
- $idList = array_map('intval', $idList);
- $idCondition = ' AND iid IN ("'.implode('","', $idList).'") ';
+ if ($c->getGradeModel()) {
+ $arr['grade_model_id'] = (int) $c->getGradeModel()->getId();
}
- $sql = "SELECT * FROM $table WHERE c_id = $courseId
- $sessionCondition
- $idCondition
- ORDER BY title ";
- $result = Database::query($sql);
+ // Evaluations
+ $arr['evaluations'] = [];
+ foreach ($c->getEvaluations() as $e) {
+ /** @var GradebookEvaluation $e */
+ $arr['evaluations'][] = [
+ 'title' => (string) $e->getTitle(),
+ 'description' => (string) ($e->getDescription() ?? ''),
+ 'weight' => (float) $e->getWeight(),
+ 'max' => (float) $e->getMax(),
+ 'type' => (string) $e->getType(),
+ 'visible' => (int) $e->getVisible(),
+ 'locked' => (int) $e->getLocked(),
+ 'best_score' => $e->getBestScore(),
+ 'average_score' => $e->getAverageScore(),
+ 'score_weight' => $e->getScoreWeight(),
+ 'min_score' => $e->getMinScore(),
+ ];
+ }
- while ($obj = Database::fetch_object($result)) {
- $forumTopic = new ForumTopic($obj);
- $this->course->add_resource($forumTopic);
- $this->build_forum_posts($courseId, $obj->thread_id, $obj->forum_id);
+ // Links
+ $arr['links'] = [];
+ foreach ($c->getLinks() as $l) {
+ /** @var GradebookLink $l */
+ $arr['links'][] = [
+ 'type' => (int) $l->getType(),
+ 'ref_id' => (int) $l->getRefId(),
+ 'weight' => (float) $l->getWeight(),
+ 'visible' => (int) $l->getVisible(),
+ 'locked' => (int) $l->getLocked(),
+ 'best_score' => $l->getBestScore(),
+ 'average_score' => $l->getAverageScore(),
+ 'score_weight' => $l->getScoreWeight(),
+ 'min_score' => $l->getMinScore(),
+ ];
}
+
+ return $arr;
}
/**
- * Build the forum-posts
- * TODO: All tree structure of posts should be built, attachments for example.
+ * Export Works (root folders only; include assignment params).
*
- * @param int $courseId Internal course ID
- * @param int $thread_id Internal thread ID
- * @param int $forum_id Internal forum ID
- * @param array $idList
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_forum_posts(
- $courseId = 0,
- $thread_id = null,
- $forum_id = null,
- $idList = []
- ) {
- $table = Database::get_course_table(TABLE_FORUM_POST);
- $courseId = (int) $courseId;
- $sql = "SELECT * FROM $table WHERE c_id = $courseId ";
- if (!empty($thread_id) && !empty($forum_id)) {
- $forum_id = (int) $forum_id;
- $thread_id = (int) $thread_id;
- $sql .= " AND thread_id = $thread_id AND forum_id = $forum_id ";
+ private function build_works(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
}
- if (!empty($idList)) {
- $idList = array_map('intval', $idList);
- $sql .= ' AND iid IN ("'.implode('","', $idList).'") ';
+ $repo = Container::getStudentPublicationRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
+
+ $qb
+ ->andWhere('resource.publicationParent IS NULL')
+ ->andWhere('resource.filetype = :ft')->setParameter('ft', 'folder')
+ ->andWhere('resource.active = 1');
+
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
}
- $sql .= ' ORDER BY post_id ASC LIMIT 1';
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $forum_post = new ForumPost($obj);
- $this->course->add_resource($forum_post);
+ /** @var CStudentPublication[] $rows */
+ $rows = $qb->getQuery()->getResult();
+
+ foreach ($rows as $row) {
+ $iid = (int) $row->getIid();
+ $title = (string) $row->getTitle();
+ $desc = (string) ($row->getDescription() ?? '');
+
+ // Detect documents linked in description
+ $this->findAndSetDocumentsInText($desc);
+
+ $asgmt = $row->getAssignment();
+ $expiresOn = $asgmt?->getExpiresOn()?->format('Y-m-d H:i:s');
+ $endsOn = $asgmt?->getEndsOn()?->format('Y-m-d H:i:s');
+ $addToCal = $asgmt && $asgmt->getEventCalendarId() > 0 ? 1 : 0;
+ $enableQ = (bool) ($asgmt?->getEnableQualification() ?? false);
+
+ $params = [
+ 'id' => $iid,
+ 'title' => $title,
+ 'description' => $desc,
+ 'weight' => (float) $row->getWeight(),
+ 'qualification' => (float) $row->getQualification(),
+ 'allow_text_assignment' => (int) $row->getAllowTextAssignment(),
+ 'default_visibility' => (bool) ($row->getDefaultVisibility() ?? false),
+ 'student_delete_own_publication' => (bool) ($row->getStudentDeleteOwnPublication() ?? false),
+ 'extensions' => $row->getExtensions(),
+ 'group_category_work_id' => (int) $row->getGroupCategoryWorkId(),
+ 'post_group_id' => (int) $row->getPostGroupId(),
+ 'enable_qualification' => $enableQ,
+ 'add_to_calendar' => $addToCal ? 1 : 0,
+ 'expires_on' => $expiresOn ?: null,
+ 'ends_on' => $endsOn ?: null,
+ 'name' => $title,
+ 'url' => null,
+ ];
+
+ $legacy = new Work($params);
+ $legacyCourse->add_resource($legacy);
}
}
/**
- * Build the links.
+ * Export Attendance + calendars.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_links(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $categories = LinkManager::getLinkCategories(
- $courseId,
- $session_id,
- $withBaseContent
- );
+ private function build_attendance(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
- // Adding empty category
- $categories[] = ['id' => 0];
+ $repo = Container::getAttendanceRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
- foreach ($categories as $category) {
- $this->build_link_category($category);
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
- $links = LinkManager::getLinksPerCategory(
- $category['id'],
- $courseId,
- $session_id,
- $withBaseContent
- );
+ /** @var CAttendance[] $rows */
+ $rows = $qb->getQuery()->getResult();
+
+ foreach ($rows as $row) {
+ $iid = (int) $row->getIid();
+ $title = (string) $row->getTitle();
+ $desc = (string) ($row->getDescription() ?? '');
+ $active = (int) $row->getActive();
+
+ $this->findAndSetDocumentsInText($desc);
+
+ $params = [
+ 'id' => $iid,
+ 'title' => $title,
+ 'description' => $desc,
+ 'active' => $active,
+ 'attendance_qualify_title' => (string) ($row->getAttendanceQualifyTitle() ?? ''),
+ 'attendance_qualify_max' => (int) $row->getAttendanceQualifyMax(),
+ 'attendance_weight' => (float) $row->getAttendanceWeight(),
+ 'locked' => (int) $row->getLocked(),
+ 'name' => $title,
+ ];
+
+ $legacy = new Attendance($params);
+
+ /** @var CAttendanceCalendar $cal */
+ foreach ($row->getCalendars() as $cal) {
+ $calArr = [
+ 'id' => (int) $cal->getIid(),
+ 'attendance_id' => $iid,
+ 'date_time' => $cal->getDateTime()?->format('Y-m-d H:i:s') ?? '',
+ 'done_attendance' => (bool) $cal->getDoneAttendance(),
+ 'blocked' => (bool) $cal->getBlocked(),
+ 'duration' => $cal->getDuration() !== null ? (int) $cal->getDuration() : null,
+ ];
+ $legacy->add_attendance_calendar($calArr);
+ }
- foreach ($links as $item) {
- if (!empty($idList)) {
- if (!in_array($item['id'], $idList)) {
- continue;
+ $legacyCourse->add_resource($legacy);
+ }
+ }
+
+ /**
+ * Export Thematic + advances + plans (and collect linked docs).
+ *
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
+ */
+ private function build_thematic(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
+
+ $repo = Container::getThematicRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
+
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
+
+ /** @var CThematic[] $rows */
+ $rows = $qb->getQuery()->getResult();
+
+ foreach ($rows as $row) {
+ $iid = (int) $row->getIid();
+ $title = (string) $row->getTitle();
+ $content = (string) ($row->getContent() ?? '');
+ $active = (bool) $row->getActive();
+
+ $this->findAndSetDocumentsInText($content);
+
+ $params = [
+ 'id' => $iid,
+ 'title' => $title,
+ 'content' => $content,
+ 'active' => $active,
+ ];
+
+ $legacy = new Thematic($params);
+
+ /** @var CThematicAdvance $adv */
+ foreach ($row->getAdvances() as $adv) {
+ $attendanceId = 0;
+ try {
+ $refAtt = new \ReflectionProperty(CThematicAdvance::class, 'attendance');
+ if ($refAtt->isInitialized($adv)) {
+ $att = $adv->getAttendance();
+ if ($att) {
+ $attendanceId = (int) $att->getIid();
+ }
}
+ } catch (\Throwable) {
+ // keep $attendanceId = 0
}
- $link = new Link(
- $item['id'],
- $item['title'],
- $item['url'],
- $item['description'],
- $item['category_id'],
- $item['on_homepage']
- );
- $link->target = $item['target'];
- $this->course->add_resource($link);
- $this->course->resources[RESOURCE_LINK][$item['id']]->add_linked_resource(
- RESOURCE_LINKCATEGORY,
- $item['category_id']
- );
+ $advArr = [
+ 'id' => (int) $adv->getIid(),
+ 'thematic_id' => (int) $row->getIid(),
+ 'content' => (string) ($adv->getContent() ?? ''),
+ 'start_date' => $adv->getStartDate()?->format('Y-m-d H:i:s') ?? '',
+ 'duration' => (int) $adv->getDuration(),
+ 'done_advance' => (bool) $adv->getDoneAdvance(),
+ 'attendance_id' => $attendanceId,
+ 'room_id' => (int) ($adv->getRoom()?->getId() ?? 0),
+ ];
+
+ $this->findAndSetDocumentsInText((string) $advArr['content']);
+ $legacy->addThematicAdvance($advArr);
+ }
+
+ /** @var CThematicPlan $pl */
+ foreach ($row->getPlans() as $pl) {
+ $plArr = [
+ 'id' => (int) $pl->getIid(),
+ 'thematic_id' => $iid,
+ 'title' => (string) $pl->getTitle(),
+ 'description' => (string) ($pl->getDescription() ?? ''),
+ 'description_type' => (int) $pl->getDescriptionType(),
+ ];
+ $this->findAndSetDocumentsInText((string) $plArr['description']);
+ $legacy->addThematicPlan($plArr);
}
+
+ $legacyCourse->add_resource($legacy);
}
}
/**
- * Build tool intro.
+ * Export Wiki pages (content + metadata; collect docs in content).
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_tool_intro(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $table = Database::get_course_table(TABLE_TOOL_INTRO);
-
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function build_wiki(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
- $courseId = (int) $courseId;
+ $repo = Container::getWikiRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
- $sql = "SELECT * FROM $table
- WHERE c_id = $courseId $sessionCondition";
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $tool_intro = new ToolIntro($obj->id, $obj->intro_text);
- $this->course->add_resource($tool_intro);
+ /** @var CWiki[] $pages */
+ $pages = $qb->getQuery()->getResult();
+
+ foreach ($pages as $page) {
+ $iid = (int) $page->getIid();
+ $pageId = (int) ($page->getPageId() ?? $iid);
+ $reflink = (string) $page->getReflink();
+ $title = (string) $page->getTitle();
+ $content = (string) $page->getContent();
+ $userId = (int) $page->getUserId();
+ $groupId = (int) ($page->getGroupId() ?? 0);
+ $progress = (string) ($page->getProgress() ?? '');
+ $version = (int) ($page->getVersion() ?? 1);
+ $dtime = $page->getDtime()?->format('Y-m-d H:i:s') ?? '';
+
+ $this->findAndSetDocumentsInText($content);
+
+ $legacy = new Wiki(
+ $iid,
+ $pageId,
+ $reflink,
+ $title,
+ $content,
+ $userId,
+ $groupId,
+ $dtime,
+ $progress,
+ $version
+ );
+
+ $this->course->add_resource($legacy);
}
}
/**
- * Build a link category.
- *
- * @param int $category Internal link ID
+ * Export Glossary terms (collect docs in descriptions).
*
- * @return int
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_link_category($category)
- {
- if (empty($category) || empty($category['category_title'])) {
- return 0;
+ private function build_glossary(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
}
- $linkCategory = new LinkCategory(
- $category['id'],
- $category['category_title'],
- $category['description'],
- $category['display_order']
- );
- $this->course->add_resource($linkCategory);
+ $repo = Container::getGlossaryRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
+
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
+
+ /** @var CGlossary[] $terms */
+ $terms = $qb->getQuery()->getResult();
+
+ foreach ($terms as $term) {
+ $iid = (int) $term->getIid();
+ $title = (string) $term->getTitle();
+ $desc = (string) ($term->getDescription() ?? '');
+
+ $this->findAndSetDocumentsInText($desc);
+
+ $legacy = new Glossary(
+ $iid,
+ $title,
+ $desc,
+ 0
+ );
- return $category['id'];
+ $this->course->add_resource($legacy);
+ }
}
/**
- * Build the Quizzes.
+ * Export Course descriptions (collect docs in HTML).
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
- * @param bool $buildOrphanQuestions
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_quizzes(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = [],
- $buildOrphanQuestions = true
- ) {
- $table_qui = Database::get_course_table(TABLE_QUIZ_TEST);
- $table_rel = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
- $table_doc = Database::get_course_table(TABLE_DOCUMENT);
-
- $courseId = (int) $courseId;
- $idCondition = '';
- if (!empty($idList)) {
- $idList = array_map('intval', $idList);
- $idCondition = ' iid IN ("'.implode('","', $idList).'") AND ';
+ private function build_course_descriptions(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
}
- if (!empty($courseId) && !empty($session_id)) {
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- true
- );
- } else {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true
- );
- }
+ $repo = Container::getCourseDescriptionRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
- // Select only quizzes with active = 0 or 1 (not -1 which is for deleted quizzes)
- $sql = "SELECT * FROM $table_qui
- WHERE
- c_id = $courseId AND
- $idCondition
- active >=0
- $sessionCondition ";
- } else {
- // Select only quizzes with active = 0 or 1 (not -1 which is for deleted quizzes)
- $sql = "SELECT * FROM $table_qui
- WHERE
- c_id = $courseId AND
- $idCondition
- active >=0 AND
- (session_id = 0 OR session_id IS NULL)";
- }
-
- $sql .= ' ORDER BY title';
- $db_result = Database::query($sql);
- $questionList = [];
- while ($obj = Database::fetch_object($db_result)) {
- if (strlen($obj->sound) > 0) {
- $sql = "SELECT id FROM $table_doc
- WHERE c_id = $courseId AND path = '/audio/".$obj->sound."'";
- $res = Database::query($sql);
- $doc = Database::fetch_object($res);
- $obj->sound = $doc->id;
- }
- $this->findAndSetDocumentsInText($obj->description);
-
- $quiz = new Quiz($obj);
- $sql = 'SELECT * FROM '.$table_rel.'
- WHERE c_id = '.$courseId.' AND quiz_id = '.$obj->id;
- $db_result2 = Database::query($sql);
- while ($obj2 = Database::fetch_object($db_result2)) {
- $quiz->add_question($obj2->question_id, $obj2->question_order);
- $questionList[] = $obj2->question_id;
- }
- $this->course->add_resource($quiz);
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
}
- if (!empty($courseId)) {
- $this->build_quiz_questions($courseId, $questionList, $buildOrphanQuestions);
- } else {
- $this->build_quiz_questions(0, $questionList, $buildOrphanQuestions);
+ /** @var CCourseDescription[] $rows */
+ $rows = $qb->getQuery()->getResult();
+
+ foreach ($rows as $row) {
+ $iid = (int) $row->getIid();
+ $title = (string) ($row->getTitle() ?? '');
+ $html = (string) ($row->getContent() ?? '');
+ $type = (int) $row->getDescriptionType();
+
+ $this->findAndSetDocumentsInText($html);
+
+ $export = new CourseDescription(
+ $iid,
+ $title,
+ $html,
+ $type
+ );
+
+ $this->course->add_resource($export);
}
}
/**
- * Build the Quiz-Questions.
+ * Export Calendar events (first attachment as legacy, all as assets).
*
- * @param int $courseId Internal course ID
- * @param array $questionList
- * @param bool $buildOrphanQuestions
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_quiz_questions($courseId = 0, $questionList = [], $buildOrphanQuestions = true)
- {
- $table_qui = Database::get_course_table(TABLE_QUIZ_TEST);
- $table_rel = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
- $table_que = Database::get_course_table(TABLE_QUIZ_QUESTION);
- $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
- $courseId = (int) $courseId;
- $questionListToString = implode("','", $questionList);
-
- // Building normal tests (many queries)
- $sql = "SELECT * FROM $table_que
- WHERE c_id = $courseId AND id IN ('$questionListToString')";
- $result = Database::query($sql);
-
- while ($obj = Database::fetch_object($result)) {
- // find the question category
- // @todo : need to be adapted for multi category questions in 1.10
- $question_category_id = TestCategory::getCategoryForQuestion(
- $obj->id,
- $courseId
- );
+ private function build_events(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
- $this->findAndSetDocumentsInText($obj->description);
-
- // build the backup resource question object
- $question = new QuizQuestion(
- $obj->id,
- $obj->question,
- $obj->description,
- $obj->ponderation,
- $obj->type,
- $obj->position,
- $obj->picture,
- $obj->level,
- $obj->extra,
- $question_category_id
- );
- $question->addPicture($this);
-
- $sql = 'SELECT * FROM '.$table_ans.'
- WHERE c_id = '.$courseId.' AND question_id = '.$obj->id;
- $db_result2 = Database::query($sql);
- while ($obj2 = Database::fetch_object($db_result2)) {
- $question->add_answer(
- $obj2->id,
- $obj2->answer,
- $obj2->correct,
- $obj2->comment,
- $obj2->ponderation,
- $obj2->position,
- $obj2->hotspot_coordinates,
- $obj2->hotspot_type
- );
+ $eventRepo = Container::getCalendarEventRepository();
+ $qb = $eventRepo->getResourcesByCourse($courseEntity, $sessionEntity);
- $this->findAndSetDocumentsInText($obj2->answer);
- $this->findAndSetDocumentsInText($obj2->comment);
-
- if (MULTIPLE_ANSWER_TRUE_FALSE == $obj->type) {
- $table_options = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
- $sql = 'SELECT * FROM '.$table_options.'
- WHERE c_id = '.$courseId.' AND question_id = '.$obj->id;
- $db_result3 = Database::query($sql);
- while ($obj3 = Database::fetch_object($db_result3)) {
- $question_option = new QuizQuestionOption($obj3);
- $question->add_option($question_option);
- }
- }
- }
- $this->course->add_resource($question);
- }
-
- if ($buildOrphanQuestions) {
- // Building a fictional test for collecting orphan questions.
- // When a course is emptied this option should be activated (true).
- //$build_orphan_questions = !empty($_POST['recycle_option']);
-
- // 1st union gets the orphan questions from deleted exercises
- // 2nd union gets the orphan questions from question that were deleted in a exercise.
- $sql = " (
- SELECT question_id, q.* FROM $table_que q
- INNER JOIN $table_rel r
- ON (q.c_id = r.c_id AND q.id = r.question_id)
- INNER JOIN $table_qui ex
- ON (ex.id = r.quiz_id AND ex.c_id = r.c_id)
- WHERE ex.c_id = $courseId AND ex.active = '-1'
- )
- UNION
- (
- SELECT question_id, q.* FROM $table_que q
- left OUTER JOIN $table_rel r
- ON (q.c_id = r.c_id AND q.id = r.question_id)
- WHERE q.c_id = $courseId AND r.question_id is null
- )
- UNION
- (
- SELECT question_id, q.* FROM $table_que q
- INNER JOIN $table_rel r
- ON (q.c_id = r.c_id AND q.id = r.question_id)
- WHERE r.c_id = $courseId AND (r.quiz_id = '-1' OR r.quiz_id = '0')
- )
- ";
-
- $result = Database::query($sql);
- if (Database::num_rows($result) > 0) {
- $orphanQuestionIds = [];
- while ($obj = Database::fetch_object($result)) {
- // Orphan questions
- if (!empty($obj->question_id)) {
- $obj->id = $obj->question_id;
- }
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
- // Avoid adding the same question twice
- if (!isset($this->course->resources[$obj->id])) {
- // find the question category
- // @todo : need to be adapted for multi category questions in 1.10
- $question_category_id = TestCategory::getCategoryForQuestion($obj->id, $courseId);
- $question = new QuizQuestion(
- $obj->id,
- $obj->question,
- $obj->description,
- $obj->ponderation,
- $obj->type,
- $obj->position,
- $obj->picture,
- $obj->level,
- $obj->extra,
- $question_category_id
- );
- $question->addPicture($this);
- $sql = "SELECT * FROM $table_ans
- WHERE c_id = $courseId AND question_id = ".$obj->id;
- $db_result2 = Database::query($sql);
- if (Database::num_rows($db_result2)) {
- while ($obj2 = Database::fetch_object($db_result2)) {
- $question->add_answer(
- $obj2->id,
- $obj2->answer,
- $obj2->correct,
- $obj2->comment,
- $obj2->ponderation,
- $obj2->position,
- $obj2->hotspot_coordinates,
- $obj2->hotspot_type
- );
+ /** @var CCalendarEvent[] $events */
+ $events = $qb->getQuery()->getResult();
+
+ /** @var KernelInterface $kernel */
+ $kernel = Container::$container->get('kernel');
+ $projectDir = rtrim($kernel->getProjectDir(), '/');
+ $resourceBase = $projectDir . '/var/upload/resource';
+
+ /** @var ResourceNodeRepository $rnRepo */
+ $rnRepo = Container::$container->get(ResourceNodeRepository::class);
+
+ foreach ($events as $ev) {
+ $iid = (int) $ev->getIid();
+ $title = (string) $ev->getTitle();
+ $content = (string) ($ev->getContent() ?? '');
+ $startDate = $ev->getStartDate()?->format('Y-m-d H:i:s') ?? '';
+ $endDate = $ev->getEndDate()?->format('Y-m-d H:i:s') ?? '';
+ $allDay = (int) $ev->isAllDay();
+
+ $firstPath = $firstName = $firstComment = '';
+ $firstSize = 0;
+
+ /** @var CCalendarEventAttachment $att */
+ foreach ($ev->getAttachments() as $att) {
+ $node = $att->getResourceNode();
+ $abs = null;
+ $size = 0;
+ $relForZip = null;
+
+ if ($node) {
+ $file = $node->getFirstResourceFile();
+ if ($file) {
+ $storedRel = (string) $rnRepo->getFilename($file);
+ if ($storedRel !== '') {
+ $candidate = $resourceBase . $storedRel;
+ if (is_readable($candidate)) {
+ $abs = $candidate;
+ $size = (int) $file->getSize();
+ if ($size <= 0 && is_file($candidate)) {
+ $st = @stat($candidate);
+ $size = $st ? (int) $st['size'] : 0;
+ }
+ $base = basename($storedRel) ?: (string) $att->getIid();
+ $relForZip = 'upload/calendar/' . $base;
}
- $orphanQuestionIds[] = $obj->id;
}
- $this->course->add_resource($question);
}
}
- }
- }
- $obj = [
- 'id' => -1,
- 'title' => get_lang('Orphan questions'),
- 'type' => 2,
- ];
- $newQuiz = new Quiz((object) $obj);
- if (!empty($orphanQuestionIds)) {
- foreach ($orphanQuestionIds as $index => $orphanId) {
- $order = $index + 1;
- $newQuiz->add_question($orphanId, $order);
+ if ($abs && $relForZip) {
+ $this->tryAddAsset($relForZip, $abs, $size);
+ } else {
+ error_log('COURSE_BUILD: event attachment file not found (event_iid='
+ . $iid . '; att_iid=' . (int) $att->getIid() . ')');
+ }
+
+ if ($firstName === '' && $relForZip) {
+ $firstPath = substr($relForZip, strlen('upload/calendar/'));
+ $firstName = (string) $att->getFilename();
+ $firstComment = (string) ($att->getComment() ?? '');
+ $firstSize = (int) $size;
+ }
}
+
+ $export = new CalendarEvent(
+ $iid,
+ $title,
+ $content,
+ $startDate,
+ $endDate,
+ $firstPath,
+ $firstName,
+ $firstSize,
+ $firstComment,
+ $allDay
+ );
+
+ $this->course->add_resource($export);
}
- $this->course->add_resource($newQuiz);
}
/**
- * @deprecated
- * Build the orphan questions
+ * Export Announcements (first attachment legacy, all as assets).
+ *
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_quiz_orphan_questions()
- {
- $table_qui = Database::get_course_table(TABLE_QUIZ_TEST);
- $table_rel = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
- $table_que = Database::get_course_table(TABLE_QUIZ_QUESTION);
- $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
-
- $courseId = api_get_course_int_id();
-
- $sql = 'SELECT *
- FROM '.$table_que.' as questions
- LEFT JOIN '.$table_rel.' as quizz_questions
- ON questions.id=quizz_questions.question_id
- LEFT JOIN '.$table_qui.' as exercises
- ON quizz_questions.quiz_id = exercises.id
- WHERE
- questions.c_id = quizz_questions.c_id AND
- questions.c_id = exercises.c_id AND
- exercises.c_id = '.$courseId.' AND
- (quizz_questions.quiz_id IS NULL OR
- exercises.active = -1)';
-
- $db_result = Database::query($sql);
- if (Database::num_rows($db_result) > 0) {
- // This is the fictional test for collecting orphan questions.
- $orphan_questions = new Quiz(
- -1,
- get_lang('Orphan questions'),
- '',
- 0,
- 0,
- 1,
- '',
- 0
- );
+ private function build_announcements(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity) {
+ return;
+ }
- $this->course->add_resource($orphan_questions);
- while ($obj = Database::fetch_object($db_result)) {
- $question = new QuizQuestion(
- $obj->id,
- $obj->question,
- $obj->description,
- $obj->ponderation,
- $obj->type,
- $obj->position,
- $obj->picture,
- $obj->level,
- $obj->extra
- );
- $question->addPicture($this);
-
- $sql = 'SELECT * FROM '.$table_ans.' WHERE question_id = '.$obj->id;
- $db_result2 = Database::query($sql);
- while ($obj2 = Database::fetch_object($db_result2)) {
- $question->add_answer(
- $obj2->id,
- $obj2->answer,
- $obj2->correct,
- $obj2->comment,
- $obj2->ponderation,
- $obj2->position,
- $obj2->hotspot_coordinates,
- $obj2->hotspot_type
- );
+ $annRepo = Container::getAnnouncementRepository();
+ $qb = $annRepo->getResourcesByCourse($courseEntity, $sessionEntity);
+
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
+ }
+
+ /** @var CAnnouncement[] $anns */
+ $anns = $qb->getQuery()->getResult();
+
+ /** @var KernelInterface $kernel */
+ $kernel = Container::$container->get('kernel');
+ $projectDir = rtrim($kernel->getProjectDir(), '/');
+ $resourceBase = $projectDir . '/var/upload/resource';
+
+ /** @var ResourceNodeRepository $rnRepo */
+ $rnRepo = Container::$container->get(ResourceNodeRepository::class);
+
+ foreach ($anns as $a) {
+ $iid = (int) $a->getIid();
+ $title = (string) $a->getTitle();
+ $html = (string) ($a->getContent() ?? '');
+ $date = $a->getEndDate()?->format('Y-m-d H:i:s') ?? '';
+ $email = (bool) $a->getEmailSent();
+
+ $firstPath = $firstName = $firstComment = '';
+ $firstSize = 0;
+
+ $attachmentsArr = [];
+
+ /** @var CAnnouncementAttachment $att */
+ foreach ($a->getAttachments() as $att) {
+ $relPath = ltrim((string) $att->getPath(), '/');
+ $assetRel = 'upload/announcements/' . $relPath;
+
+ $abs = null;
+ $node = $att->getResourceNode();
+ if ($node) {
+ $file = $node->getFirstResourceFile();
+ if ($file) {
+ $storedRel = (string) $rnRepo->getFilename($file);
+ if ($storedRel !== '') {
+ $candidate = $resourceBase . $storedRel;
+ if (is_readable($candidate)) {
+ $abs = $candidate;
+ }
+ }
+ }
+ }
+
+ if ($abs) {
+ $this->tryAddAsset($assetRel, $abs, (int) $att->getSize());
+ } else {
+ error_log('COURSE_BUILD: announcement attachment not found (iid=' . (int) $att->getIid() . ')');
+ }
+
+ $attachmentsArr[] = [
+ 'path' => $relPath,
+ 'filename' => (string) $att->getFilename(),
+ 'size' => (int) $att->getSize(),
+ 'comment' => (string) ($att->getComment() ?? ''),
+ 'asset_relpath' => $assetRel,
+ ];
+
+ if ($firstName === '') {
+ $firstPath = $relPath;
+ $firstName = (string) $att->getFilename();
+ $firstSize = (int) $att->getSize();
+ $firstComment = (string) ($att->getComment() ?? '');
}
- $this->course->add_resource($question);
}
+
+ $payload = [
+ 'title' => $title,
+ 'content' => $html,
+ 'date' => $date,
+ 'display_order' => 0,
+ 'email_sent' => $email ? 1 : 0,
+ 'attachment_path' => $firstPath,
+ 'attachment_filename' => $firstName,
+ 'attachment_size' => $firstSize,
+ 'attachment_comment' => $firstComment,
+ 'attachments' => $attachmentsArr,
+ ];
+
+ $legacyCourse->resources[RESOURCE_ANNOUNCEMENT][$iid] =
+ $this->mkLegacyItem(RESOURCE_ANNOUNCEMENT, $iid, $payload, ['attachments']);
}
}
/**
- * Build the test category.
+ * Register an asset to be packed into the export ZIP.
*
- * @param int $sessionId Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
+ * @param string $relPath Relative path inside the ZIP
+ * @param string $absPath Absolute filesystem path
+ * @param int $size
+ * @return void
+ */
+ private function addAsset(string $relPath, string $absPath, int $size = 0): void
+ {
+ if (!isset($this->course->resources['asset']) || !is_array($this->course->resources['asset'])) {
+ $this->course->resources['asset'] = [];
+ }
+ $this->course->resources['asset'][$relPath] = [
+ 'abs' => $absPath,
+ 'size' => $size,
+ ];
+ }
+
+ /**
+ * Try to add an asset only if file exists.
*
- * @todo add course session
+ * @param string $relPath
+ * @param string $absPath
+ * @param int $size
+ * @return void
*/
- public function build_test_category(
- $sessionId = 0,
- $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- // get all test category in course
- $category = new TestCategory();
- $categories = $category->getCategories();
- foreach ($categories as $category) {
- $this->findAndSetDocumentsInText($category->getDescription());
- /** @var TestCategory $category */
- $courseCopyTestCategory = new CourseCopyTestCategory(
- $category->id,
- $category->name,
- $category->description
- );
- $this->course->add_resource($courseCopyTestCategory);
+ private function tryAddAsset(string $relPath, string $absPath, int $size = 0): void
+ {
+ if (is_file($absPath) && is_readable($absPath)) {
+ $this->addAsset($relPath, $absPath, $size);
+ } else {
+ error_log('COURSE_BUILD: asset missing: ' . $absPath);
}
}
/**
- * Build the Surveys.
+ * Export Surveys; returns needed Question IDs for follow-up export.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $surveyIds
+ * @return array
*/
- public function build_surveys(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table_survey = Database::get_course_table(TABLE_SURVEY);
- $table_question = Database::get_course_table(TABLE_SURVEY_QUESTION);
-
- $courseId = (int) $courseId;
-
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function build_surveys(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $surveyIds
+ ): array {
+ if (!$courseEntity) {
+ return [];
+ }
- $sql = 'SELECT * FROM '.$table_survey.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- if ($id_list) {
- $sql .= ' AND iid IN ('.implode(', ', $id_list).')';
- }
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $survey = new Survey(
- $obj->survey_id,
- $obj->code,
- $obj->title,
- $obj->subtitle,
- $obj->author,
- $obj->lang,
- $obj->avail_from,
- $obj->avail_till,
- $obj->is_shared,
- $obj->template,
- $obj->intro,
- $obj->surveythanks,
- $obj->creation_date,
- $obj->invited,
- $obj->answered,
- $obj->invite_mail,
- $obj->reminder_mail,
- $obj->one_question_per_page,
- $obj->shuffle
- );
- $sql = 'SELECT * FROM '.$table_question.'
- WHERE c_id = '.$courseId.' AND survey_id = '.$obj->survey_id;
- $db_result2 = Database::query($sql);
- while ($obj2 = Database::fetch_object($db_result2)) {
- $survey->add_question($obj2->question_id);
+ $qb = $this->em->createQueryBuilder()
+ ->select('s')
+ ->from(CSurvey::class, 's')
+ ->innerJoin('s.resourceNode', 'rn')
+ ->leftJoin('rn.resourceLinks', 'links')
+ ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
+ ->andWhere($sessionEntity
+ ? '(links.session IS NULL OR links.session = :session)'
+ : 'links.session IS NULL'
+ )
+ ->andWhere('links.deletedAt IS NULL')
+ ->andWhere('links.endVisibilityAt IS NULL');
+
+ if ($sessionEntity) {
+ $qb->setParameter('session', $sessionEntity);
+ }
+ if (!empty($surveyIds)) {
+ $qb->andWhere('s.iid IN (:ids)')->setParameter('ids', array_map('intval', $surveyIds));
+ }
+
+ /** @var CSurvey[] $surveys */
+ $surveys = $qb->getQuery()->getResult();
+
+ $neededQuestionIds = [];
+
+ foreach ($surveys as $s) {
+ $iid = (int) $s->getIid();
+ $qIds = [];
+
+ foreach ($s->getQuestions() as $q) {
+ /** @var CSurveyQuestion $q */
+ $qid = (int) $q->getIid();
+ $qIds[] = $qid;
+ $neededQuestionIds[$qid] = true;
}
- $this->course->add_resource($survey);
+
+ $payload = [
+ 'code' => (string) ($s->getCode() ?? ''),
+ 'title' => (string) $s->getTitle(),
+ 'subtitle' => (string) ($s->getSubtitle() ?? ''),
+ 'author' => '',
+ 'lang' => (string) ($s->getLang() ?? ''),
+ 'avail_from' => $s->getAvailFrom()?->format('Y-m-d H:i:s'),
+ 'avail_till' => $s->getAvailTill()?->format('Y-m-d H:i:s'),
+ 'is_shared' => (string) ($s->getIsShared() ?? '0'),
+ 'template' => (string) ($s->getTemplate() ?? 'template'),
+ 'intro' => (string) ($s->getIntro() ?? ''),
+ 'surveythanks' => (string) ($s->getSurveythanks() ?? ''),
+ 'creation_date' => $s->getCreationDate()?->format('Y-m-d H:i:s') ?: date('Y-m-d H:i:s'),
+ 'invited' => (int) $s->getInvited(),
+ 'answered' => (int) $s->getAnswered(),
+ 'invite_mail' => (string) $s->getInviteMail(),
+ 'reminder_mail' => (string) $s->getReminderMail(),
+ 'mail_subject' => (string) $s->getMailSubject(),
+ 'anonymous' => (string) $s->getAnonymous(),
+ 'shuffle' => (bool) $s->getShuffle(),
+ 'one_question_per_page' => (bool) $s->getOneQuestionPerPage(),
+ 'visible_results' => $s->getVisibleResults(),
+ 'display_question_number' => (bool) $s->isDisplayQuestionNumber(),
+ 'survey_type' => (int) $s->getSurveyType(),
+ 'show_form_profile' => (int) $s->getShowFormProfile(),
+ 'form_fields' => (string) $s->getFormFields(),
+ 'duration' => $s->getDuration(),
+ 'question_ids' => $qIds,
+ 'survey_id' => $iid,
+ ];
+
+ $legacyCourse->resources[RESOURCE_SURVEY][$iid] =
+ $this->mkLegacyItem(RESOURCE_SURVEY, $iid, $payload);
+
+ error_log('COURSE_BUILD: SURVEY iid=' . $iid . ' qids=[' . implode(',', $qIds) . ']');
}
- $this->build_survey_questions($courseId);
+
+ return array_keys($neededQuestionIds);
}
/**
- * Build the Survey Questions.
+ * Export Survey Questions (answers promoted at top level).
*
- * @param int $courseId Internal course ID
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $questionIds
+ * @return void
*/
- public function build_survey_questions($courseId)
- {
- $table_que = Database::get_course_table(TABLE_SURVEY_QUESTION);
- $table_opt = Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION);
+ private function build_survey_questions(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $questionIds
+ ): void {
+ if (!$courseEntity) {
+ return;
+ }
- $courseId = (int) $courseId;
- $idList = isset($this->specific_id_list['surveys']) ? $this->specific_id_list['surveys'] : [];
+ $qb = $this->em->createQueryBuilder()
+ ->select('q', 's')
+ ->from(CSurveyQuestion::class, 'q')
+ ->innerJoin('q.survey', 's')
+ ->innerJoin('s.resourceNode', 'rn')
+ ->leftJoin('rn.resourceLinks', 'links')
+ ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
+ ->andWhere($sessionEntity
+ ? '(links.session IS NULL OR links.session = :session)'
+ : 'links.session IS NULL'
+ )
+ ->andWhere('links.deletedAt IS NULL')
+ ->andWhere('links.endVisibilityAt IS NULL')
+ ->orderBy('s.iid', 'ASC')
+ ->addOrderBy('q.sort', 'ASC');
+
+ if ($sessionEntity) {
+ $qb->setParameter('session', $sessionEntity);
+ }
+ if (!empty($questionIds)) {
+ $qb->andWhere('q.iid IN (:ids)')->setParameter('ids', array_map('intval', $questionIds));
+ }
- $sql = 'SELECT * FROM '.$table_que.' WHERE c_id = '.$courseId.' ';
+ /** @var CSurveyQuestion[] $questions */
+ $questions = $qb->getQuery()->getResult();
- if (!empty($idList)) {
- $sql .= ' AND survey_id IN ('.implode(', ', $idList).')';
- }
+ $exported = 0;
- $db_result = Database::query($sql);
- $is_required = 0;
- while ($obj = Database::fetch_object($db_result)) {
- if (isset($obj->is_required)) {
- $is_required = $obj->is_required;
- }
- $question = new SurveyQuestion(
- $obj->question_id,
- $obj->survey_id,
- $obj->survey_question,
- $obj->survey_question_comment,
- $obj->type,
- $obj->display,
- $obj->sort,
- $obj->shared_question_id,
- $obj->max_value,
- $is_required
- );
- $sql = 'SELECT * FROM '.$table_opt.'
- WHERE c_id = '.$courseId.' AND question_id = '.$obj->question_id;
- $db_result2 = Database::query($sql);
- while ($obj2 = Database::fetch_object($db_result2)) {
- $question->add_answer($obj2->option_text, $obj2->sort);
+ foreach ($questions as $q) {
+ $qid = (int) $q->getIid();
+ $sid = (int) $q->getSurvey()->getIid();
+
+ $answers = [];
+ foreach ($q->getOptions() as $opt) {
+ /** @var CSurveyQuestionOption $opt */
+ $answers[] = [
+ 'option_text' => (string) $opt->getOptionText(),
+ 'sort' => (int) $opt->getSort(),
+ 'value' => (int) $opt->getValue(),
+ ];
}
- $this->course->add_resource($question);
+
+ $payload = [
+ 'survey_id' => $sid,
+ 'survey_question' => (string) $q->getSurveyQuestion(),
+ 'survey_question_comment' => (string) ($q->getSurveyQuestionComment() ?? ''),
+ 'type' => (string) $q->getType(),
+ 'display' => (string) $q->getDisplay(),
+ 'sort' => (int) $q->getSort(),
+ 'shared_question_id' => $q->getSharedQuestionId(),
+ 'max_value' => $q->getMaxValue(),
+ 'is_required' => (bool) $q->isMandatory(),
+ 'answers' => $answers,
+ ];
+
+ $legacyCourse->resources[RESOURCE_SURVEYQUESTION][$qid] =
+ $this->mkLegacyItem(RESOURCE_SURVEYQUESTION, $qid, $payload, ['answers']);
+
+ $exported++;
+ error_log('COURSE_BUILD: SURVEY_Q qid=' . $qid . ' survey=' . $sid . ' answers=' . count($answers));
}
+
+ error_log('COURSE_BUILD: survey questions exported=' . $exported);
}
/**
- * Build the announcements.
+ * Export Quizzes and return required Question IDs.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $quizIds
+ * @return array
*/
- public function build_announcements(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table = Database::get_course_table(TABLE_ANNOUNCEMENT);
-
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function build_quizzes(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $quizIds
+ ): array {
+ if (!$courseEntity) {
+ return [];
+ }
- $courseId = (int) $courseId;
+ $qb = $this->em->createQueryBuilder()
+ ->select('q')
+ ->from(CQuiz::class, 'q')
+ ->innerJoin('q.resourceNode', 'rn')
+ ->leftJoin('rn.resourceLinks', 'links')
+ ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
+ ->andWhere($sessionEntity
+ ? '(links.session IS NULL OR links.session = :session)'
+ : 'links.session IS NULL'
+ )
+ ->andWhere('links.deletedAt IS NULL')
+ ->andWhere('links.endVisibilityAt IS NULL');
+
+ if ($sessionEntity) {
+ $qb->setParameter('session', $sessionEntity);
+ }
+ if (!empty($quizIds)) {
+ $qb->andWhere('q.iid IN (:ids)')->setParameter('ids', array_map('intval', $quizIds));
+ }
- $sql = 'SELECT * FROM '.$table.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- $db_result = Database::query($sql);
- $table_attachment = Database::get_course_table(
- TABLE_ANNOUNCEMENT_ATTACHMENT
- );
- while ($obj = Database::fetch_object($db_result)) {
- if (empty($obj->id)) {
- continue;
- }
- $sql = 'SELECT path, comment, filename, size
- FROM '.$table_attachment.'
- WHERE c_id = '.$courseId.' AND announcement_id = '.$obj->id.'';
- $result = Database::query($sql);
- $attachment_obj = Database::fetch_object($result);
- $att_path = $att_filename = $att_size = $atth_comment = '';
-
- if (!empty($attachment_obj)) {
- $att_path = $attachment_obj->path;
- $att_filename = $attachment_obj->filename;
- $att_size = $attachment_obj->size;
- $atth_comment = $attachment_obj->comment;
+ /** @var CQuiz[] $quizzes */
+ $quizzes = $qb->getQuery()->getResult();
+ $neededQuestionIds = [];
+
+ foreach ($quizzes as $quiz) {
+ $iid = (int) $quiz->getIid();
+
+ $payload = [
+ 'title' => (string) $quiz->getTitle(),
+ 'description' => (string) ($quiz->getDescription() ?? ''),
+ 'type' => (int) $quiz->getType(),
+ 'random' => (int) $quiz->getRandom(),
+ 'random_answers' => (bool) $quiz->getRandomAnswers(),
+ 'results_disabled' => (int) $quiz->getResultsDisabled(),
+ 'max_attempt' => (int) $quiz->getMaxAttempt(),
+ 'feedback_type' => (int) $quiz->getFeedbackType(),
+ 'expired_time' => (int) $quiz->getExpiredTime(),
+ 'review_answers' => (int) $quiz->getReviewAnswers(),
+ 'random_by_category' => (int) $quiz->getRandomByCategory(),
+ 'text_when_finished' => (string) ($quiz->getTextWhenFinished() ?? ''),
+ 'text_when_finished_failure' => (string) ($quiz->getTextWhenFinishedFailure() ?? ''),
+ 'display_category_name' => (int) $quiz->getDisplayCategoryName(),
+ 'save_correct_answers' => (int) ($quiz->getSaveCorrectAnswers() ?? 0),
+ 'propagate_neg' => (int) $quiz->getPropagateNeg(),
+ 'hide_question_title' => (bool) $quiz->isHideQuestionTitle(),
+ 'hide_question_number' => (int) $quiz->getHideQuestionNumber(),
+ 'question_selection_type' => (int) ($quiz->getQuestionSelectionType() ?? 0),
+ 'access_condition' => (string) ($quiz->getAccessCondition() ?? ''),
+ 'pass_percentage' => $quiz->getPassPercentage(),
+ 'start_time' => $quiz->getStartTime()?->format('Y-m-d H:i:s'),
+ 'end_time' => $quiz->getEndTime()?->format('Y-m-d H:i:s'),
+ 'question_ids' => [],
+ 'question_orders' => [],
+ ];
+
+ $rels = $this->em->createQueryBuilder()
+ ->select('rel', 'qq')
+ ->from(CQuizRelQuestion::class, 'rel')
+ ->innerJoin('rel.question', 'qq')
+ ->andWhere('rel.quiz = :quiz')
+ ->setParameter('quiz', $quiz)
+ ->orderBy('rel.questionOrder', 'ASC')
+ ->getQuery()->getResult();
+
+ foreach ($rels as $rel) {
+ $qid = (int) $rel->getQuestion()->getIid();
+ $payload['question_ids'][] = $qid;
+ $payload['question_orders'][] = (int) $rel->getQuestionOrder();
+ $neededQuestionIds[$qid] = true;
}
- $announcement = new Announcement(
- $obj->id,
- $obj->title,
- $obj->content,
- $obj->end_date,
- $obj->display_order,
- $obj->email_sent,
- $att_path,
- $att_filename,
- $att_size,
- $atth_comment
- );
- $this->course->add_resource($announcement);
+ $legacyCourse->resources[RESOURCE_QUIZ][$iid] =
+ $this->mkLegacyItem(
+ RESOURCE_QUIZ,
+ $iid,
+ $payload,
+ ['question_ids', 'question_orders']
+ );
}
+
+ error_log(
+ 'COURSE_BUILD: build_quizzes done; total=' . count($quizzes)
+ );
+
+ return array_keys($neededQuestionIds);
}
/**
- * Build the events.
+ * Safe count helper for mixed values.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * @param mixed $v
+ * @return int
*/
- public function build_events(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table = Database::get_course_table(TABLE_AGENDA);
-
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
+ private function safeCount(mixed $v): int
+ {
+ return (is_array($v) || $v instanceof \Countable) ? \count($v) : 0;
+ }
- $courseId = (int) $courseId;
+ /**
+ * Export Quiz Questions (answers and options promoted).
+ *
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $questionIds
+ * @return void
+ */
+ private function build_quiz_questions(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $questionIds
+ ): void {
+ if (!$courseEntity) {
+ return;
+ }
- $sql = 'SELECT * FROM '.$table.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $table_attachment = Database::get_course_table(
- TABLE_AGENDA_ATTACHMENT
- );
- $sql = 'SELECT path, comment, filename, size
- FROM '.$table_attachment.'
- WHERE c_id = '.$courseId.' AND agenda_id = '.$obj->id.'';
- $result = Database::query($sql);
-
- $attachment_obj = Database::fetch_object($result);
- $att_path = $att_filename = $att_size = $atth_comment = '';
- if (!empty($attachment_obj)) {
- $att_path = $attachment_obj->path;
- $att_filename = $attachment_obj->filename;
- $att_size = $attachment_obj->size;
- $atth_comment = $attachment_obj->comment;
- }
- $event = new CalendarEvent(
- $obj->id,
- $obj->title,
- $obj->content,
- $obj->start_date,
- $obj->end_date,
- $att_path,
- $att_filename,
- $att_size,
- $atth_comment,
- $obj->all_day
- );
- $this->course->add_resource($event);
+ error_log('COURSE_BUILD: build_quiz_questions start ids=' . json_encode(array_values($questionIds)));
+ error_log('COURSE_BUILD: build_quiz_questions exported=' . $this->safeCount($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? 0));
+
+ $qb = $this->em->createQueryBuilder()
+ ->select('qq')
+ ->from(CQuizQuestion::class, 'qq')
+ ->innerJoin('qq.resourceNode', 'qrn')
+ ->leftJoin('qrn.resourceLinks', 'qlinks')
+ ->andWhere('qlinks.course = :course')->setParameter('course', $courseEntity)
+ ->andWhere($sessionEntity
+ ? '(qlinks.session IS NULL OR qlinks.session = :session)'
+ : 'qlinks.session IS NULL'
+ )
+ ->andWhere('qlinks.deletedAt IS NULL')
+ ->andWhere('qlinks.endVisibilityAt IS NULL');
+
+ if ($sessionEntity) {
+ $qb->setParameter('session', $sessionEntity);
+ }
+ if (!empty($questionIds)) {
+ $qb->andWhere('qq.iid IN (:ids)')->setParameter('ids', array_map('intval', $questionIds));
}
+
+ /** @var CQuizQuestion[] $questions */
+ $questions = $qb->getQuery()->getResult();
+
+ error_log('COURSE_BUILD: build_quiz_questions start ids=' . json_encode(array_values($questionIds)));
+ error_log('COURSE_BUILD: build_quiz_questions exported=' . $this->safeCount($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? 0));
+
+ $this->exportQuestionsWithAnswers($legacyCourse, $questions);
}
/**
- * Build the course-descriptions.
+ * Internal exporter for quiz questions + answers (+ options for MATF type).
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param array $questions
+ * @return void
*/
- public function build_course_descriptions(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
- $courseId = (int) $courseId;
-
- if (!empty($session_id) && !empty($courseId)) {
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- true
- );
- } else {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true
- );
+ private function exportQuestionsWithAnswers(object $legacyCourse, array $questions): void
+ {
+ foreach ($questions as $q) {
+ $qid = (int) $q->getIid();
+
+ $payload = [
+ 'question' => (string) $q->getQuestion(),
+ 'description' => (string) ($q->getDescription() ?? ''),
+ 'ponderation' => (float) $q->getPonderation(),
+ 'position' => (int) $q->getPosition(),
+ 'type' => (int) $q->getType(),
+ 'quiz_type' => (int) $q->getType(),
+ 'picture' => (string) ($q->getPicture() ?? ''),
+ 'level' => (int) $q->getLevel(),
+ 'extra' => (string) ($q->getExtra() ?? ''),
+ 'feedback' => (string) ($q->getFeedback() ?? ''),
+ 'question_code' => (string) ($q->getQuestionCode() ?? ''),
+ 'mandatory' => (int) $q->getMandatory(),
+ 'duration' => $q->getDuration(),
+ 'parent_media_id'=> $q->getParentMediaId(),
+ 'answers' => [],
+ ];
+
+ $ans = $this->em->createQueryBuilder()
+ ->select('a')
+ ->from(CQuizAnswer::class, 'a')
+ ->andWhere('a.question = :q')->setParameter('q', $q)
+ ->orderBy('a.position', 'ASC')
+ ->getQuery()->getResult();
+
+ foreach ($ans as $a) {
+ $payload['answers'][] = [
+ 'id' => (int) $a->getIid(),
+ 'answer' => (string) $a->getAnswer(),
+ 'comment' => (string) ($a->getComment() ?? ''),
+ 'ponderation' => (float) $a->getPonderation(),
+ 'position' => (int) $a->getPosition(),
+ 'hotspot_coordinates' => $a->getHotspotCoordinates(),
+ 'hotspot_type' => $a->getHotspotType(),
+ 'correct' => $a->getCorrect(),
+ ];
}
- $sql = 'SELECT * FROM '.$table.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- } else {
- $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
- $sql = 'SELECT * FROM '.$table.'
- WHERE c_id = '.$courseId.' AND session_id = 0';
- }
-
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $cd = new CourseDescription(
- $obj->id,
- $obj->title,
- $obj->content,
- $obj->description_type
+
+ if (defined('MULTIPLE_ANSWER_TRUE_FALSE') && MULTIPLE_ANSWER_TRUE_FALSE === (int) $q->getType()) {
+ $opts = $this->em->createQueryBuilder()
+ ->select('o')
+ ->from(CQuizQuestionOption::class, 'o')
+ ->andWhere('o.question = :q')->setParameter('q', $q)
+ ->orderBy('o.position', 'ASC')
+ ->getQuery()->getResult();
+
+ $payload['question_options'] = array_map(static fn($o) => [
+ 'id' => (int) $o->getIid(),
+ 'name' => (string) $o->getTitle(),
+ 'position' => (int) $o->getPosition(),
+ ], $opts);
+ }
+
+ $legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid] =
+ $this->mkLegacyItem(RESOURCE_QUIZQUESTION, $qid, $payload, ['answers', 'question_options']);
+
+ error_log('COURSE_BUILD: QQ qid=' . $qid .
+ ' quiz_type=' . ($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->quiz_type ?? 'missing') .
+ ' answers=' . count($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->answers ?? [])
);
- $this->course->add_resource($cd);
}
}
/**
- * @param int $session_id
- * @param int $courseId
- * @param bool $withBaseContent
- * @param array $idList
+ * Export Link category as legacy item.
+ *
+ * @param CLinkCategory $category
+ * @return void
*/
- public function build_learnpath_category($session_id = 0, $courseId = 0, $withBaseContent = false, $idList = [])
+ private function build_link_category(CLinkCategory $category): void
{
- $categories = learnpath::getCategories($courseId);
-
- /** @var CLpCategory $item */
- foreach ($categories as $item) {
- $categoryId = $item->getId();
- if (!empty($idList)) {
- if (!in_array($categoryId, $idList)) {
- continue;
- }
- }
- $category = new LearnPathCategory($categoryId, $item);
- $this->course->add_resource($category);
+ $id = (int) $category->getIid();
+ if ($id <= 0) {
+ return;
}
+
+ $payload = [
+ 'title' => (string) $category->getTitle(),
+ 'description' => (string) ($category->getDescription() ?? ''),
+ 'category_title'=> (string) $category->getTitle(),
+ ];
+
+ $this->course->resources[RESOURCE_LINKCATEGORY][$id] =
+ $this->mkLegacyItem(RESOURCE_LINKCATEGORY, $id, $payload);
}
/**
- * Build the learnpaths.
+ * Export Links (and their categories once).
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
- * @param bool $addScormFolder
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_learnpaths(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = [],
- $addScormFolder = true
- ) {
- $table_main = Database::get_course_table(TABLE_LP_MAIN);
- $table_item = Database::get_course_table(TABLE_LP_ITEM);
- $table_tool = Database::get_course_table(TABLE_TOOL_LIST);
-
- $courseId = (int) $courseId;
-
- if (!empty($session_id) && !empty($courseId)) {
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- true
- );
- } else {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true
- );
- }
- $sql = 'SELECT * FROM '.$table_main.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- } else {
- $sql = 'SELECT * FROM '.$table_main.'
- WHERE c_id = '.$courseId.' AND (session_id = 0 OR session_id IS NULL)';
- }
-
- if (!empty($id_list)) {
- $id_list = array_map('intval', $id_list);
- $sql .= ' AND id IN ('.implode(', ', $id_list).') ';
- }
-
- $result = Database::query($sql);
- if ($result) {
- while ($obj = Database::fetch_object($result)) {
- $items = [];
- $sql = "SELECT * FROM $table_item
- WHERE c_id = '$courseId' AND lp_id = ".$obj->id;
- $resultItem = Database::query($sql);
- while ($obj_item = Database::fetch_object($resultItem)) {
- $item['id'] = $obj_item->iid;
- $item['item_type'] = $obj_item->item_type;
- $item['ref'] = $obj_item->ref;
- $item['title'] = $obj_item->title;
- $item['description'] = $obj_item->description;
- $item['path'] = $obj_item->path;
- $item['min_score'] = $obj_item->min_score;
- $item['max_score'] = $obj_item->max_score;
- $item['mastery_score'] = $obj_item->mastery_score;
- $item['parent_item_id'] = $obj_item->parent_item_id;
- $item['previous_item_id'] = $obj_item->previous_item_id;
- $item['next_item_id'] = $obj_item->next_item_id;
- $item['display_order'] = $obj_item->display_order;
- $item['prerequisite'] = $obj_item->prerequisite;
- $item['parameters'] = $obj_item->parameters;
- $item['launch_data'] = $obj_item->launch_data;
- $item['audio'] = $obj_item->audio;
- $items[] = $item;
- }
+ private function build_links(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
- $sql = "SELECT id FROM $table_tool
- WHERE
- c_id = $courseId AND
- (link LIKE '%lp_controller.php%lp_id=".$obj->id."%' AND image='scormbuilder.gif') AND
- visibility = '1' ";
- $db_tool = Database::query($sql);
- $visibility = '0';
- if (Database::num_rows($db_tool)) {
- $visibility = '1';
- }
+ $linkRepo = Container::getLinkRepository();
+ $catRepo = Container::getLinkCategoryRepository();
- $lp = new CourseCopyLearnpath(
- $obj->id,
- $obj->lp_type,
- $obj->title,
- $obj->path,
- $obj->ref,
- $obj->description,
- $obj->content_local,
- $obj->default_encoding,
- $obj->default_view_mod,
- $obj->prevent_reinit,
- $obj->force_commit,
- $obj->content_maker,
- $obj->display_order,
- $obj->js_lib,
- $obj->content_license,
- $obj->debug,
- $visibility,
- $obj->author,
- //$obj->preview_image,
- $obj->use_max_score,
- $obj->autolaunch,
- $obj->created_on,
- $obj->modified_on,
- $obj->published_on,
- $obj->expired_on,
- $obj->session_id,
- $obj->category_id,
- $items
- );
+ $qb = $linkRepo->getResourcesByCourse($courseEntity, $sessionEntity);
- $this->course->add_resource($lp);
-
- /*if (!empty($obj->preview_image)) {
- // Add LP teacher image
- $asset = new Asset(
- $obj->preview_image,
- '/upload/learning_path/images/'.$obj->preview_image,
- '/upload/learning_path/images/'.$obj->preview_image
- );
- $this->course->add_resource($asset);
- }*/
- }
+ if (!empty($ids)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
}
- // Save scorm directory (previously build_scorm_documents())
- if ($addScormFolder) {
- $i = 1;
- if ($dir = @opendir($this->course->backup_path.'/scorm')) {
- while ($file = readdir($dir)) {
- if (is_dir($this->course->backup_path.'/scorm/'.$file) &&
- !in_array($file, ['.', '..'])
- ) {
- $doc = new ScormDocument($i++, '/'.$file, $file);
- $this->course->add_resource($doc);
- }
- }
- closedir($dir);
+ /** @var CLink[] $links */
+ $links = $qb->getQuery()->getResult();
+
+ $exportedCats = [];
+
+ foreach ($links as $link) {
+ $iid = (int) $link->getIid();
+ $title = (string) $link->getTitle();
+ $url = (string) $link->getUrl();
+ $desc = (string) ($link->getDescription() ?? '');
+ $tgt = (string) ($link->getTarget() ?? '');
+
+ $cat = $link->getCategory();
+ $catId = (int) ($cat?->getIid() ?? 0);
+
+ if ($catId > 0 && !isset($exportedCats[$catId])) {
+ $this->build_link_category($cat);
+ $exportedCats[$catId] = true;
}
+
+ $payload = [
+ 'title' => $title !== '' ? $title : $url,
+ 'url' => $url,
+ 'description' => $desc,
+ 'target' => $tgt,
+ 'category_id' => $catId,
+ 'on_homepage' => false,
+ ];
+
+ $legacyCourse->resources[RESOURCE_LINK][$iid] =
+ $this->mkLegacyItem(RESOURCE_LINK, $iid, $payload);
}
}
/**
- * Build the glossaries.
+ * Format DateTime as string "Y-m-d H:i:s".
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * @param \DateTimeInterface|null $dt
+ * @return string
*/
- public function build_glossary(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table_glossary = Database::get_course_table(TABLE_GLOSSARY);
-
- $courseId = (int) $courseId;
-
- if (!empty($session_id) && !empty($courseId)) {
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- true
- );
- } else {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true
- );
- }
+ private function fmtDate(?\DateTimeInterface $dt): string
+ {
+ return $dt ? $dt->format('Y-m-d H:i:s') : '';
+ }
- //@todo check this queries are the same ...
- if (!empty($this->course->type) && 'partial' == $this->course->type) {
- $sql = 'SELECT * FROM '.$table_glossary.' g
- WHERE g.c_id = '.$courseId.' '.$sessionCondition;
- } else {
- $sql = 'SELECT * FROM '.$table_glossary.' g
- WHERE g.c_id = '.$courseId.' '.$sessionCondition;
- }
- } else {
- $table_glossary = Database::get_course_table(TABLE_GLOSSARY);
- //@todo check this queries are the same ... ayayay
- if (!empty($this->course->type) && 'partial' == $this->course->type) {
- $sql = 'SELECT * FROM '.$table_glossary.' g
- WHERE g.c_id = '.$courseId.' AND (session_id = 0 OR session_id IS NULL)';
- } else {
- $sql = 'SELECT * FROM '.$table_glossary.' g
- WHERE g.c_id = '.$courseId.' AND (session_id = 0 OR session_id IS NULL)';
+ /**
+ * Create a legacy item object, promoting selected array keys to top-level.
+ *
+ * @param string $type
+ * @param int $sourceId
+ * @param array|object $obj
+ * @param array $arrayKeysToPromote
+ * @return \stdClass
+ */
+ private function mkLegacyItem(string $type, int $sourceId, array|object $obj, array $arrayKeysToPromote = []): \stdClass
+ {
+ $o = new \stdClass();
+ $o->type = $type;
+ $o->source_id = $sourceId;
+ $o->destination_id = -1;
+ $o->has_obj = true;
+ $o->obj = (object) $obj;
+
+ foreach ((array) $obj as $k => $v) {
+ if (is_scalar($v) || $v === null) {
+ if (!property_exists($o, $k)) {
+ $o->$k = $v;
+ }
}
}
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $doc = new Glossary(
- $obj->glossary_id,
- $obj->name,
- $obj->description,
- $obj->display_order
- );
- $this->course->add_resource($doc);
+ foreach ($arrayKeysToPromote as $k) {
+ if (isset($obj[$k]) && is_array($obj[$k])) {
+ $o->$k = $obj[$k];
+ }
}
- }
- /*
- * Build session course by jhon
- */
- public function build_session_course()
- {
- $tbl_session = Database::get_main_table(TABLE_MAIN_SESSION);
- $tbl_session_course = Database::get_main_table(TABLE_MAIN_SESSION_COURSE);
- $list_course = CourseManager::get_course_list();
- $list = [];
- foreach ($list_course as $_course) {
- $this->course = new Course();
- $this->course->code = $_course['code'];
- $this->course->type = 'partial';
- $this->course->path = api_get_path(SYS_COURSE_PATH).$_course['directory'].'/';
- $this->course->backup_path = api_get_path(SYS_COURSE_PATH).$_course['directory'];
- $this->course->encoding = api_get_system_encoding(); //current platform encoding
- $courseId = $_course['real_id'];
- $sql = "SELECT s.id, name, c_id
- FROM $tbl_session_course sc
- INNER JOIN $tbl_session s
- ON sc.session_id = s.id
- WHERE sc.c_id = '$courseId' ";
- $query_session = Database::query($sql);
- while ($rows_session = Database::fetch_assoc($query_session)) {
- $session = new CourseSession(
- $rows_session['id'],
- $rows_session['name']
- );
- $this->course->add_resource($session);
+ // Ensure "name" for restorer display
+ if (!isset($o->name) || $o->name === '' || $o->name === null) {
+ if (isset($obj['name']) && $obj['name'] !== '') {
+ $o->name = (string) $obj['name'];
+ } elseif (isset($obj['title']) && $obj['title'] !== '') {
+ $o->name = (string) $obj['title'];
+ } else {
+ $o->name = $type . ' ' . $sourceId;
}
- $list[] = $this->course;
}
- return $list;
+ return $o;
}
/**
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * Build an id filter closure.
+ *
+ * @param array $idsFilter
+ * @return \Closure(int):bool
*/
- public function build_wiki(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $tbl_wiki = Database::get_course_table(TABLE_WIKI);
- $courseId = (int) $courseId;
-
- if (!empty($session_id) && !empty($courseId)) {
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- true
- );
- } else {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true
- );
- }
- $sql = 'SELECT * FROM '.$tbl_wiki.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- } else {
- $tbl_wiki = Database::get_course_table(TABLE_WIKI);
- $sql = 'SELECT * FROM '.$tbl_wiki.'
- WHERE c_id = '.$courseId.' AND (session_id = 0 OR session_id IS NULL)';
- }
- $db_result = Database::query($sql);
- while ($obj = Database::fetch_object($db_result)) {
- $wiki = new Wiki(
- $obj->id,
- $obj->page_id,
- $obj->reflink,
- $obj->title,
- $obj->content,
- $obj->user_id,
- $obj->group_id,
- $obj->dtime,
- $obj->progress,
- $obj->version
- );
- $this->course->add_resource($wiki);
+ private function makeIdFilter(array $idsFilter): \Closure
+ {
+ if (empty($idsFilter)) {
+ return static fn(int $id): bool => true;
}
+ $set = array_fill_keys(array_map('intval', $idsFilter), true);
+ return static fn(int $id): bool => isset($set[$id]);
}
/**
- * Build the Surveys.
+ * Export Tool intro (tool -> intro text) for visible tools.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
+ * @param object $legacyCourse
+ * @param CourseEntity|null $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @return void
*/
- public function build_thematic(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table_thematic = Database::get_course_table(TABLE_THEMATIC);
- $table_thematic_advance = Database::get_course_table(TABLE_THEMATIC_ADVANCE);
- $table_thematic_plan = Database::get_course_table(TABLE_THEMATIC_PLAN);
- $courseId = (int) $courseId;
-
- $courseInfo = api_get_course_info_by_id($courseId);
- $session_id = (int) $session_id;
- if ($withBaseContent) {
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- true
- );
+ private function build_tool_intro(
+ object $legacyCourse,
+ ?CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity
+ ): void {
+ if (!$courseEntity instanceof CourseEntity) {
+ return;
+ }
+
+ $repo = $this->em->getRepository(CToolIntro::class);
+
+ $qb = $repo->createQueryBuilder('ti')
+ ->innerJoin('ti.courseTool', 'ct')
+ ->andWhere('ct.course = :course')
+ ->setParameter('course', $courseEntity);
+
+ if ($sessionEntity) {
+ $qb->andWhere('ct.session = :session')->setParameter('session', $sessionEntity);
} else {
- $sessionCondition = api_get_session_condition($session_id, true);
+ $qb->andWhere('ct.session IS NULL');
}
- $sql = "SELECT * FROM $table_thematic
- WHERE c_id = $courseId $sessionCondition ";
- $db_result = Database::query($sql);
- while ($row = Database::fetch_assoc($db_result)) {
- $thematic = new Thematic($row);
- $sql = 'SELECT * FROM '.$table_thematic_advance.'
- WHERE c_id = '.$courseId.' AND thematic_id = '.$row['id'];
+ $qb->andWhere('ct.visibility = :vis')->setParameter('vis', true);
- $result = Database::query($sql);
- while ($sub_row = Database::fetch_assoc($result)) {
- $thematic->addThematicAdvance($sub_row);
+ /** @var CToolIntro[] $intros */
+ $intros = $qb->getQuery()->getResult();
+
+ foreach ($intros as $intro) {
+ $ctool = $intro->getCourseTool(); // CTool
+ $titleKey = (string) $ctool->getTitle(); // e.g. 'documents', 'forum'
+ if ($titleKey === '') {
+ continue;
}
- $items = api_get_item_property_by_tool(
- 'thematic_plan',
- $courseInfo['code'],
- $session_id
- );
+ $payload = [
+ 'id' => $titleKey,
+ 'intro_text' => (string) $intro->getIntroText(),
+ ];
- $thematic_plan_id_list = [];
- if (!empty($items)) {
- foreach ($items as $item) {
- $thematic_plan_id_list[] = $item['ref'];
- }
+ // Use 0 as source_id (unused by restore)
+ $legacyCourse->resources[RESOURCE_TOOL_INTRO][$titleKey] =
+ $this->mkLegacyItem(RESOURCE_TOOL_INTRO, 0, $payload);
+ }
+ }
+
+ /**
+ * Export Forum categories.
+ *
+ * @param object $legacyCourse
+ * @param CourseEntity $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
+ */
+ private function build_forum_category(
+ object $legacyCourse,
+ CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ $repo = Container::getForumCategoryRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
+ $categories = $qb->getQuery()->getResult();
+
+ $keep = $this->makeIdFilter($ids);
+
+ foreach ($categories as $cat) {
+ /** @var CForumCategory $cat */
+ $id = (int) $cat->getIid();
+ if (!$keep($id)) {
+ continue;
}
- if (count($thematic_plan_id_list) > 0) {
- $sql = "SELECT tp.*
- FROM $table_thematic_plan tp
- INNER JOIN $table_thematic t ON (t.id=tp.thematic_id)
- WHERE
- t.c_id = $courseId AND
- tp.c_id = $courseId AND
- thematic_id = {$row['id']} AND
- tp.id IN (".implode(', ', $thematic_plan_id_list).') ';
-
- $result = Database::query($sql);
- while ($sub_row = Database::fetch_assoc($result)) {
- $thematic->addThematicPlan($sub_row);
- }
+
+ $payload = [
+ 'title' => (string) $cat->getTitle(),
+ 'description' => (string) ($cat->getCatComment() ?? ''),
+ 'cat_title' => (string) $cat->getTitle(),
+ 'cat_comment' => (string) ($cat->getCatComment() ?? ''),
+ ];
+
+ $legacyCourse->resources[RESOURCE_FORUMCATEGORY][$id] =
+ $this->mkLegacyItem(RESOURCE_FORUMCATEGORY, $id, $payload);
+ }
+ }
+
+ /**
+ * Export Forums.
+ *
+ * @param object $legacyCourse
+ * @param CourseEntity $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
+ */
+ private function build_forums(
+ object $legacyCourse,
+ CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ $repo = Container::getForumRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
+ $forums = $qb->getQuery()->getResult();
+
+ $keep = $this->makeIdFilter($ids);
+
+ foreach ($forums as $f) {
+ /** @var CForum $f */
+ $id = (int) $f->getIid();
+ if (!$keep($id)) {
+ continue;
}
- $this->course->add_resource($thematic);
+
+ $payload = [
+ 'title' => (string) $f->getTitle(),
+ 'description' => (string) ($f->getForumComment() ?? ''),
+ 'forum_title' => (string) $f->getTitle(),
+ 'forum_comment' => (string) ($f->getForumComment() ?? ''),
+ 'forum_category' => (int) ($f->getForumCategory()?->getIid() ?? 0),
+ 'allow_anonymous' => (int) ($f->getAllowAnonymous() ?? 0),
+ 'allow_edit' => (int) ($f->getAllowEdit() ?? 0),
+ 'approval_direct_post' => (string) ($f->getApprovalDirectPost() ?? '0'),
+ 'allow_attachments' => (int) ($f->getAllowAttachments() ?? 1),
+ 'allow_new_threads' => (int) ($f->getAllowNewThreads() ?? 1),
+ 'default_view' => (string) ($f->getDefaultView() ?? 'flat'),
+ 'forum_of_group' => (string) ($f->getForumOfGroup() ?? '0'),
+ 'forum_group_public_private' => (string) ($f->getForumGroupPublicPrivate() ?? 'public'),
+ 'moderated' => (int) ($f->isModerated() ? 1 : 0),
+ 'start_time' => $this->fmtDate($f->getStartTime()),
+ 'end_time' => $this->fmtDate($f->getEndTime()),
+ ];
+
+ $legacyCourse->resources[RESOURCE_FORUM][$id] =
+ $this->mkLegacyItem(RESOURCE_FORUM, $id, $payload);
}
}
/**
- * Build the attendances.
+ * Export Forum threads.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $id_list If you want to restrict the structure to only the given IDs
- * @throws \Exception
- * @throws Exception
+ * @param object $legacyCourse
+ * @param CourseEntity $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_attendance(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false,
- $id_list = []
- ) {
- $table_attendance = Database::get_course_table(TABLE_ATTENDANCE);
- $table_attendance_calendar = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR);
- $sessionCondition = api_get_session_condition($session_id, true, $withBaseContent);
- $courseId = (int) $courseId;
-
- $sql = 'SELECT * FROM '.$table_attendance.'
- WHERE c_id = '.$courseId.' '.$sessionCondition;
- $db_result = Database::query($sql);
- while ($row = Database::fetch_assoc($db_result)) {
- $obj = new Attendance($row);
- $sql = 'SELECT * FROM '.$table_attendance_calendar.'
- WHERE c_id = '.$courseId.' AND attendance_id = '.$row['id'];
-
- $result = Database::query($sql);
- while ($sub_row = Database::fetch_assoc($result)) {
- $obj->add_attendance_calendar($sub_row);
+ private function build_forum_topics(
+ object $legacyCourse,
+ CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ $repo = Container::getForumThreadRepository();
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
+ $threads = $qb->getQuery()->getResult();
+
+ $keep = $this->makeIdFilter($ids);
+
+ foreach ($threads as $t) {
+ /** @var CForumThread $t */
+ $id = (int) $t->getIid();
+ if (!$keep($id)) {
+ continue;
}
- $this->course->add_resource($obj);
+
+ $payload = [
+ 'title' => (string) $t->getTitle(),
+ 'thread_title' => (string) $t->getTitle(),
+ 'title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
+ 'topic_poster_name' => (string) ($t->getUser()?->getUsername() ?? ''),
+ 'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
+ 'thread_date' => $this->fmtDate($t->getThreadDate()),
+ 'thread_sticky' => (int) ($t->getThreadSticky() ? 1 : 0),
+ 'thread_title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
+ 'thread_qualify_max' => (float) $t->getThreadQualifyMax(),
+ 'thread_weight' => (float) $t->getThreadWeight(),
+ 'thread_peer_qualify' => (int) ($t->isThreadPeerQualify() ? 1 : 0),
+ ];
+
+ $legacyCourse->resources[RESOURCE_FORUMTOPIC][$id] =
+ $this->mkLegacyItem(RESOURCE_FORUMTOPIC, $id, $payload);
}
}
/**
- * Build the works (or "student publications", or "assignments").
+ * Export first post for each thread as topic root post.
*
- * @param int $session_id Internal session ID
- * @param int $courseId Internal course ID
- * @param bool $withBaseContent Whether to include content from the course without session or not
- * @param array $idList If you want to restrict the structure to only the given IDs
- * @throws Exception
+ * @param object $legacyCourse
+ * @param CourseEntity $courseEntity
+ * @param SessionEntity|null $sessionEntity
+ * @param array $ids
+ * @return void
*/
- public function build_works(
- int $session_id = 0,
- int $courseId = 0,
- $withBaseContent = false,
- $idList = []
- ) {
- $table_work = Database::get_course_table(TABLE_STUDENT_PUBLICATION);
- $sessionCondition = api_get_session_condition(
- $session_id,
- true,
- $withBaseContent
- );
- $courseId = (int) $courseId;
+ private function build_forum_posts(
+ object $legacyCourse,
+ CourseEntity $courseEntity,
+ ?SessionEntity $sessionEntity,
+ array $ids
+ ): void {
+ $repoThread = Container::getForumThreadRepository();
+ $repoPost = Container::getForumPostRepository();
+
+ $qb = $repoThread->getResourcesByCourse($courseEntity, $sessionEntity);
+ $threads = $qb->getQuery()->getResult();
+
+ $keep = $this->makeIdFilter($ids);
+
+ foreach ($threads as $t) {
+ /** @var CForumThread $t */
+ $threadId = (int) $t->getIid();
+ if (!$keep($threadId)) {
+ continue;
+ }
- $idCondition = '';
- if (!empty($idList)) {
- $idList = array_map('intval', $idList);
- $idCondition = ' AND iid IN ("'.implode('","', $idList).'") ';
- }
-
- $sql = "SELECT * FROM $table_work
- WHERE
- c_id = $courseId
- $sessionCondition AND
- filetype = 'folder' AND
- parent_id = 0 AND
- active = 1
- $idCondition
- ";
- $result = Database::query($sql);
- while ($row = Database::fetch_assoc($result)) {
- $obj = new Work($row);
- $this->course->add_resource($obj);
+ $first = $repoPost->findOneBy(['thread' => $t], ['postDate' => 'ASC', 'iid' => 'ASC']);
+ if (!$first) {
+ continue;
+ }
+
+ $postId = (int) $first->getIid();
+ $titleFromPost = trim((string) $first->getTitle());
+ if ($titleFromPost === '') {
+ $plain = trim(strip_tags((string) ($first->getPostText() ?? '')));
+ $titleFromPost = mb_substr($plain !== '' ? $plain : 'Post', 0, 60);
+ }
+
+ $payload = [
+ 'title' => $titleFromPost,
+ 'post_title' => $titleFromPost,
+ 'post_text' => (string) ($first->getPostText() ?? ''),
+ 'thread_id' => $threadId,
+ 'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
+ 'post_notification' => (int) ($first->getPostNotification() ? 1 : 0),
+ 'visible' => (int) ($first->getVisible() ? 1 : 0),
+ 'status' => (int) ($first->getStatus() ?? CForumPost::STATUS_VALIDATED),
+ 'post_parent_id' => (int) ($first->getPostParent()?->getIid() ?? 0),
+ 'poster_id' => (int) ($first->getUser()?->getId() ?? 0),
+ 'text' => (string) ($first->getPostText() ?? ''),
+ 'poster_name' => (string) ($first->getUser()?->getUsername() ?? ''),
+ 'post_date' => $this->fmtDate($first->getPostDate()),
+ ];
+
+ $legacyCourse->resources[RESOURCE_FORUMPOST][$postId] =
+ $this->mkLegacyItem(RESOURCE_FORUMPOST, $postId, $payload);
}
}
+ /* -----------------------------------------------------------------
+ * Documents (Chamilo 2 style)
+ * ----------------------------------------------------------------- */
+
/**
- * @param int $session_id
- * @param int $courseId
- * @param bool $withBaseContent
+ * New Chamilo 2 build: CDocumentRepository-based (instead of legacy tables).
+ *
+ * @param CourseEntity|null $course
+ * @param SessionEntity|null $session
+ * @param bool $withBaseContent
+ * @param array $idList
+ * @return void
*/
- public function build_gradebook(
- $session_id = 0,
- $courseId = 0,
- $withBaseContent = false
- ) {
- $courseInfo = api_get_course_info_by_id($courseId);
- $courseCode = $courseInfo['code'];
- $cats = Category::load(
- null,
- null,
- $courseCode,
+ private function build_documents_with_repo(
+ ?CourseEntity $course,
+ ?SessionEntity $session,
+ bool $withBaseContent,
+ array $idList = []
+ ): void {
+ if (!$course instanceof CourseEntity) {
+ return;
+ }
+
+ $qb = $this->docRepo->getResourcesByCourse(
+ $course,
+ $session,
null,
null,
- $session_id
+ true,
+ false
);
- if (!empty($cats)) {
- /** @var Category $cat */
- foreach ($cats as $cat) {
- $cat->evaluations = $cat->get_evaluations(null, false);
- $cat->links = $cat->get_links(null, false);
- $cat->subCategories = $cat->get_subcategories(
- null,
- $courseCode,
- $session_id
- );
+ if (!empty($idList)) {
+ $qb->andWhere('resource.iid IN (:ids)')
+ ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))));
+ }
+
+ /** @var CDocument[] $docs */
+ $docs = $qb->getQuery()->getResult();
+
+ foreach ($docs as $doc) {
+ $node = $doc->getResourceNode();
+ $filetype = $doc->getFiletype(); // 'file'|'folder'|...
+ $title = $doc->getTitle();
+ $comment = $doc->getComment() ?? '';
+ $iid = (int) $doc->getIid();
+ $fullPath = $doc->getFullPath();
+
+ // Determine size
+ $size = 0;
+ if ($filetype === 'folder') {
+ $size = $this->docRepo->getFolderSize($node, $course, $session);
+ } else {
+ /** @var Collection|null $files */
+ $files = $node?->getResourceFiles();
+ if ($files && $files->count() > 0) {
+ /** @var ResourceFile $first */
+ $first = $files->first();
+ $size = (int) $first->getSize();
+ }
}
- $obj = new GradeBookBackup($cats);
- $this->course->add_resource($obj);
+
+ $exportDoc = new Document(
+ $iid,
+ '/' . $fullPath,
+ $comment,
+ $title,
+ $filetype,
+ (string) $size
+ );
+
+ $this->course->add_resource($exportDoc);
}
}
+
+ /**
+ * Backward-compatible wrapper for build_documents_with_repo().
+ *
+ * @param int $session_id
+ * @param int $courseId
+ * @param bool $withBaseContent
+ * @param array $idList
+ * @return void
+ */
+ public function build_documents(
+ int $session_id = 0,
+ int $courseId = 0,
+ bool $withBaseContent = false,
+ array $idList = []
+ ): void {
+ /** @var CourseEntity|null $course */
+ $course = $this->em->getRepository(CourseEntity::class)->find($courseId);
+ /** @var SessionEntity|null $session */
+ $session = $session_id ? $this->em->getRepository(SessionEntity::class)->find($session_id) : null;
+
+ $this->build_documents_with_repo($course, $session, $withBaseContent, $idList);
+ }
}
diff --git a/src/CourseBundle/Component/CourseCopy/CourseRecycler.php b/src/CourseBundle/Component/CourseCopy/CourseRecycler.php
index c543657d592..2dfffe83dbb 100644
--- a/src/CourseBundle/Component/CourseCopy/CourseRecycler.php
+++ b/src/CourseBundle/Component/CourseCopy/CourseRecycler.php
@@ -2,779 +2,312 @@
/* For licensing terms, see /license.txt */
-namespace Chamilo\CourseBundle\Component\CourseCopy;
+declare(strict_types=1);
-use CourseManager;
-use Database;
-use TestCategory;
+namespace Chamilo\CourseBundle\Component\CourseCopy;
-/**
- * Class to delete items from a Chamilo-course.
- *
- * @author Bart Mollet
- */
-class CourseRecycler
+use Chamilo\CoreBundle\Entity\AbstractResource;
+use Chamilo\CourseBundle\Entity\CAnnouncement;
+use Chamilo\CourseBundle\Entity\CAttendance;
+use Chamilo\CourseBundle\Entity\CCalendarEvent;
+use Chamilo\CourseBundle\Entity\CCourseDescription;
+use Chamilo\CourseBundle\Entity\CDocument;
+use Chamilo\CourseBundle\Entity\CForum;
+use Chamilo\CourseBundle\Entity\CForumCategory;
+use Chamilo\CourseBundle\Entity\CGlossary;
+use Chamilo\CourseBundle\Entity\CLink;
+use Chamilo\CourseBundle\Entity\CLinkCategory;
+use Chamilo\CourseBundle\Entity\CLp;
+use Chamilo\CourseBundle\Entity\CLpCategory;
+use Chamilo\CourseBundle\Entity\CQuiz;
+use Chamilo\CourseBundle\Entity\CQuizCategory;
+use Chamilo\CourseBundle\Entity\CStudentPublication;
+use Chamilo\CourseBundle\Entity\CSurvey;
+use Chamilo\CourseBundle\Entity\CThematic;
+use Chamilo\CourseBundle\Entity\CWiki;
+use Chamilo\CoreBundle\Entity\Course as CoreCourse;
+use Doctrine\ORM\EntityManagerInterface;
+
+final class CourseRecycler
{
- /**
- * A course-object with the items to delete.
- */
- public $course;
- public $type;
-
- /**
- * Create a new CourseRecycler.
- *
- * @param course $course The course-object which contains the items to
- * delete
- */
- public function __construct($course)
+ public function __construct(
+ private readonly EntityManagerInterface $em,
+ private readonly string $courseCode,
+ private readonly int $courseId
+ ) {}
+
+ /** $type: 'full_backup' | 'select_items' ; $selected: [type => [id => true]] */
+ public function recycle(string $type, array $selected): void
{
- $this->course = $course;
- $this->course_info = api_get_course_info($this->course->code);
- $this->course_id = $this->course_info['real_id'];
+ $isFull = ($type === 'full_backup');
+
+ // If your EM doesn't have wrapInTransaction(), replace by $this->em->transactional(fn() => { ... })
+ $this->em->wrapInTransaction(function () use ($isFull, $selected) {
+ // Links & categories
+ $this->recycleGeneric($isFull, CLink::class, $selected['link'] ?? []);
+ $this->recycleGeneric($isFull, CLinkCategory::class, $selected['link_category'] ?? [], autoClean: true);
+
+ // Calendar & announcements
+ $this->recycleGeneric($isFull, CCalendarEvent::class, $selected['event'] ?? []);
+ $this->recycleGeneric($isFull, CAnnouncement::class, $selected['announcement'] ?? []);
+
+ // Documents
+ $this->recycleGeneric($isFull, CDocument::class, $selected['document'] ?? [], deleteFiles: true);
+
+ // Forums & forum categories
+ $this->recycleGeneric($isFull, CForum::class, $selected['forum'] ?? [], cascadeHeavy: true);
+ $this->recycleGeneric($isFull, CForumCategory::class, $selected['forum_category'] ?? [], autoClean: true);
+
+ // Quizzes & categories
+ $this->recycleGeneric($isFull, CQuiz::class, $selected['quiz'] ?? [], cascadeHeavy: true);
+ $this->recycleGeneric($isFull, CQuizCategory::class, $selected['test_category'] ?? []);
+
+ // Surveys
+ $this->recycleGeneric($isFull, CSurvey::class, $selected['survey'] ?? [], cascadeHeavy: true);
+
+ // Learning paths & categories
+ $this->recycleGeneric($isFull, CLp::class, $selected['learnpath'] ?? [], cascadeHeavy: true, scormCleanup: true);
+ $this->recycleLpCategories($isFull, $selected['learnpath_category'] ?? []);
+
+ // Other resources
+ $this->recycleGeneric($isFull, CCourseDescription::class, $selected['course_description'] ?? []);
+ $this->recycleGeneric($isFull, CWiki::class, $selected['wiki'] ?? [], cascadeHeavy: true);
+ $this->recycleGeneric($isFull, CGlossary::class, $selected['glossary'] ?? []);
+ $this->recycleGeneric($isFull, CThematic::class, $selected['thematic'] ?? [], cascadeHeavy: true);
+ $this->recycleGeneric($isFull, CAttendance::class, $selected['attendance'] ?? [], cascadeHeavy: true);
+ $this->recycleGeneric($isFull, CStudentPublication::class, $selected['work'] ?? [], cascadeHeavy: true);
+
+ if ($isFull) {
+ // If you keep cleaning course picture:
+ // CourseManager::deleteCoursePicture($this->courseCode);
+ }
+ });
}
/**
- * Delete all items from the course.
- * This deletes all items in the course-object from the current Chamilo-
- * course.
- *
- * @param string $backupType 'full_backup' or 'select_items'
- *
- * @return bool
- *
- * @assert (null) === false
+ * Generic recycler for any AbstractResource-based entity.
+ * - If $isFull => deletes *all resources of that type* for the course.
+ * - If partial => deletes only the provided $ids.
+ * Options:
+ * - deleteFiles: physical files are already handled by hardDelete (if repo supports it).
+ * - cascadeHeavy: for heavy-relations types (forums, LPs). hardDelete should traverse.
+ * - autoClean: e.g. remove empty categories after deleting links/forums.
+ * - scormCleanup: if LP SCORM → hook storage service if needed.
*/
- public function recycle($backupType)
- {
- if (empty($backupType)) {
- return false;
+ private function recycleGeneric(
+ bool $isFull,
+ string $entityClass,
+ array $idsMap,
+ bool $deleteFiles = false,
+ bool $cascadeHeavy = false,
+ bool $autoClean = false,
+ bool $scormCleanup = false
+ ): void {
+ if ($isFull) {
+ $this->deleteAllOfTypeForCourse($entityClass);
+ if ($autoClean) {
+ $this->autoCleanIfSupported($entityClass);
+ }
+ if ($scormCleanup && $entityClass === CLp::class) {
+ $this->cleanupScormDirsForAllLp();
+ }
+ return;
}
- $table_tool_intro = Database::get_course_table(TABLE_TOOL_INTRO);
- $table_item_properties = Database::get_course_table(TABLE_ITEM_PROPERTY);
-
- $this->type = $backupType;
- $this->recycle_links();
- $this->recycle_link_categories();
- $this->recycle_events();
- $this->recycle_announcements();
- $this->recycle_documents();
- $this->recycle_forums();
- $this->recycle_forum_categories();
- $this->recycle_quizzes();
- $this->recycle_test_category();
- $this->recycle_surveys();
- $this->recycle_learnpaths();
- $this->recycle_learnpath_categories();
- $this->recycle_cours_description();
- $this->recycle_wiki();
- $this->recycle_glossary();
- $this->recycle_thematic();
- $this->recycle_attendance();
- $this->recycle_work();
-
- foreach ($this->course->resources as $type => $resources) {
- foreach ($resources as $id => $resource) {
- if (is_numeric($id)) {
- $sql = "DELETE FROM $table_item_properties
- WHERE c_id = ".$this->course_id." AND tool ='".$resource->get_tool()."' AND ref=".$id;
- Database::query($sql);
- } elseif (RESOURCE_TOOL_INTRO == $type) {
- $sql = "DELETE FROM $table_tool_intro
- WHERE c_id = ".$this->course_id." AND id='$id'";
- Database::query($sql);
- }
+ $ids = $this->ids($idsMap);
+ if (!$ids) {
+ if ($autoClean) {
+ $this->autoCleanIfSupported($entityClass);
}
+ return;
}
- if ('full_backup' === $backupType) {
- CourseManager::deleteCoursePicture($this->course_info['code']);
- }
- }
+ $this->deleteSelectedOfTypeForCourse($entityClass, $ids);
- /**
- * Delete documents.
- */
- public function recycle_documents()
- {
- $table = Database::get_course_table(TABLE_DOCUMENT);
- $tableItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
-
- if ('full_backup' === $this->type) {
- $sql = "DELETE FROM $tableItemProperty
- WHERE
- c_id = ".$this->course_id." AND
- tool = '".TOOL_DOCUMENT."'";
- Database::query($sql);
-
- $sql = "DELETE FROM $table WHERE c_id = ".$this->course_id;
- Database::query($sql);
-
- // Delete all content in the documents.
- rmdirr($this->course->backup_path.'/document', true);
- } else {
- if ($this->course->has_resources(RESOURCE_DOCUMENT)) {
- foreach ($this->course->resources[RESOURCE_DOCUMENT] as $document) {
- rmdirr($this->course->backup_path.'/'.$document->path);
- }
-
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_DOCUMENT])));
- if (!empty($ids)) {
- $sql = "DELETE FROM $table
- WHERE c_id = ".$this->course_id.' AND id IN('.$ids.')';
- Database::query($sql);
- }
- }
+ if ($autoClean) {
+ $this->autoCleanIfSupported($entityClass);
+ }
+ if ($scormCleanup && $entityClass === CLp::class) {
+ $this->cleanupScormDirsForLpIds($ids);
}
}
- /**
- * Delete wiki.
- */
- public function recycle_wiki()
+ /** LP categories: detach LPs and then delete selected/all categories */
+ private function recycleLpCategories(bool $isFull, array $idsMap): void
{
- if ($this->course->has_resources(RESOURCE_WIKI)) {
- $table_wiki = Database::get_course_table(TABLE_WIKI);
- $table_wiki_conf = Database::get_course_table(TABLE_WIKI_CONF);
- $pages = [];
- foreach ($this->course->resources[RESOURCE_WIKI] as $resource) {
- $pages[] = $resource->page_id;
- }
-
- $wiki_ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_WIKI])));
- if (!empty($wiki_ids)) {
- $page_ids = implode(',', $pages);
-
- $sql = 'DELETE FROM '.$table_wiki.'
- WHERE c_id = '.$this->course_id.' AND id IN('.$wiki_ids.')';
- Database::query($sql);
+ if ($isFull) {
+ // Detach all categories from LPs in course
+ $this->clearLpCategoriesForCourse();
+ $this->deleteAllOfTypeForCourse(CLpCategory::class);
+ return;
+ }
- $sql = 'DELETE FROM '.$table_wiki_conf.'
- WHERE c_id = '.$this->course_id.' AND page_id IN('.$page_ids.')';
- Database::query($sql);
- }
+ $ids = $this->ids($idsMap);
+ if (!$ids) {
+ return;
}
+
+ // Detach LPs from these categories
+ $this->clearLpCategoriesForIds($ids);
+ $this->deleteSelectedOfTypeForCourse(CLpCategory::class, $ids);
}
- /**
- * Delete glossary.
- */
- public function recycle_glossary()
+ /** Normalizes IDs from [id => true] maps into int/string scalars */
+ private function ids(array $map): array
{
- if ($this->course->has_resources(RESOURCE_GLOSSARY)) {
- $table = Database::get_course_table(TABLE_GLOSSARY);
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_GLOSSARY])));
- if (!empty($ids)) {
- $sql = "DELETE FROM $table
- WHERE c_id = ".$this->course_id.' AND glossary_id IN('.$ids.')';
- Database::query($sql);
- }
- }
+ return array_values(array_filter(array_map(
+ static fn($k) => is_numeric($k) ? (int) $k : (string) $k,
+ array_keys($map)
+ ), static fn($v) => $v !== '' && $v !== null));
}
- /**
- * Delete links.
- */
- public function recycle_links()
+ /** Lightweight Course reference for query builders */
+ private function courseRef(): CoreCourse
{
- if ($this->course->has_resources(RESOURCE_LINK)) {
- $table = Database::get_course_table(TABLE_LINK);
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_LINK])));
- if (!empty($ids)) {
- $sql = "DELETE FROM $table
- WHERE c_id = ".$this->course_id.' AND id IN('.$ids.')';
- Database::query($sql);
- }
- }
+ /** @var CoreCourse $ref */
+ $ref = $this->em->getReference(CoreCourse::class, $this->courseId);
+ return $ref;
}
/**
- * Delete forums.
+ * Fetches resources by entity class within course, optionally filtering by resource iid.
+ * If the repository doesn't extend ResourceRepository, falls back to a generic QB.
+ *
+ * @return array
*/
- public function recycle_forums()
+ private function fetchResourcesForCourse(string $entityClass, ?array $ids = null): array
{
- $table_category = Database::get_course_table(TABLE_FORUM_CATEGORY);
- $table_forum = Database::get_course_table(TABLE_FORUM);
- $table_thread = Database::get_course_table(TABLE_FORUM_THREAD);
- $table_post = Database::get_course_table(TABLE_FORUM_POST);
- $table_attachment = Database::get_course_table(TABLE_FORUM_ATTACHMENT);
- $table_notification = Database::get_course_table(TABLE_FORUM_NOTIFICATION);
- $table_mail_queue = Database::get_course_table(TABLE_FORUM_MAIL_QUEUE);
- $table_thread_qualify = Database::get_course_table(TABLE_FORUM_THREAD_QUALIFY);
- $table_thread_qualify_log = Database::get_course_table(TABLE_FORUM_THREAD_QUALIFY_LOG);
-
- if ('full_backup' === $this->type) {
- $sql = 'DELETE FROM '.$table_category.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_forum.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_thread.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_post.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_attachment.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_notification.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_mail_queue.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_thread_qualify.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_thread_qualify_log.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_thread_qualify_log.' WHERE c_id = '.$this->course_id;
- Database::query($sql);
- }
+ $repo = $this->em->getRepository($entityClass);
- if ($this->course->has_resources(RESOURCE_FORUMCATEGORY)) {
- $forum_ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_FORUMCATEGORY])));
- if (!empty($forum_ids)) {
- $sql = 'DELETE FROM '.$table_category.'
- WHERE c_id = '.$this->course_id.' AND cat_id IN('.$forum_ids.');';
- Database::query($sql);
+ // Path A: repository exposes ResourceRepository API
+ if (method_exists($repo, 'getResourcesByCourseIgnoreVisibility')) {
+ $qb = $repo->getResourcesByCourseIgnoreVisibility($this->courseRef());
+ if ($ids && \count($ids) > 0) {
+ $qb->andWhere('resource.iid IN (:ids)')->setParameter('ids', $ids);
}
+ return $qb->getQuery()->getResult();
}
- if ($this->course->has_resources(RESOURCE_FORUM)) {
- $forum_ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_FORUM])));
-
- if (empty($forum_ids)) {
- return false;
- }
-
- $sql = "DELETE FROM $table_attachment USING $table_attachment
- INNER JOIN $table_post
- WHERE ".$table_post.'.c_id = '.$this->course_id.' AND
- '.$table_attachment.'.c_id = '.$this->course_id.' AND
- '.$table_attachment.'.post_id = '.$table_post.'.post_id'.
- ' AND '.$table_post.'.forum_id IN('.$forum_ids.');';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_mail_queue.' USING '.$table_mail_queue." INNER JOIN $table_post
- WHERE
- ".$table_post.'.c_id = '.$this->course_id.' AND
- '.$table_mail_queue.'.c_id = '.$this->course_id.' AND
- '.$table_mail_queue.'.post_id = '.$table_post.'.post_id AND
- '.$table_post.'.forum_id IN('.$forum_ids.');';
- Database::query($sql);
-
- // Just in case, deleting in the same table using thread_id as record-linker.
- $sql = "DELETE FROM $table_mail_queue
- USING ".$table_mail_queue." INNER JOIN $table_thread
- WHERE
- $table_mail_queue.c_id = ".$this->course_id." AND
- $table_thread.c_id = ".$this->course_id." AND
- $table_mail_queue.thread_id = ".$table_thread.".thread_id AND
- $table_thread.forum_id IN(".$forum_ids.');';
- Database::query($sql);
-
- $sql = "DELETE FROM $table_thread_qualify
- USING $table_thread_qualify INNER JOIN $table_thread
- WHERE
- $table_thread_qualify.c_id = ".$this->course_id." AND
- $table_thread.c_id = ".$this->course_id." AND
- $table_thread_qualify.thread_id = $table_thread.thread_id AND
- $table_thread.forum_id IN(".$forum_ids.');';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_thread_qualify_log.
- ' USING '.$table_thread_qualify_log.' INNER JOIN '.$table_thread.
- " WHERE
- $table_thread_qualify_log.c_id = ".$this->course_id." AND
- $table_thread.c_id = ".$this->course_id.' AND
- '.$table_thread_qualify_log.'.thread_id = '.$table_thread.'.thread_id AND
- '.$table_thread.'.forum_id IN('.$forum_ids.');';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_notification.'
- WHERE c_id = '.$this->course_id.' AND forum_id IN('.$forum_ids.')';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_post.'
- WHERE c_id = '.$this->course_id.' AND forum_id IN('.$forum_ids.')';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_thread.'
- WHERE c_id = '.$this->course_id.' AND forum_id IN('.$forum_ids.')';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_forum.'
- WHERE c_id = '.$this->course_id.' AND forum_id IN('.$forum_ids.')';
- Database::query($sql);
+ // Path B: generic fallback (join to ResourceNode/ResourceLinks and filter by course)
+ $qb = $this->em->createQueryBuilder()
+ ->select('resource')
+ ->from($entityClass, 'resource')
+ ->innerJoin('resource.resourceNode', 'node')
+ ->innerJoin('node.resourceLinks', 'links')
+ ->andWhere('links.course = :course')
+ ->setParameter('course', $this->courseRef());
+
+ if ($ids && \count($ids) > 0) {
+ $qb->andWhere('resource.iid IN (:ids)')->setParameter('ids', $ids);
}
- }
-
- /**
- * Deletes all forum-categories without forum from the current course.
- * Categories with forums in it are dealt with by recycle_forums()
- * This requires a check on the status of the forum item in c_item_property.
- */
- public function recycle_forum_categories()
- {
- $forumTable = Database::get_course_table(TABLE_FORUM);
- $forumCategoryTable = Database::get_course_table(TABLE_FORUM_CATEGORY);
- $itemPropertyTable = Database::get_course_table(TABLE_ITEM_PROPERTY);
- $courseId = $this->course_id;
- // c_forum_forum.forum_category points to c_forum_category.cat_id and
- // has to be queried *with* the c_id to ensure a match
- $subQuery = "SELECT distinct(f.forum_category) as categoryId
- FROM $forumTable f, $itemPropertyTable i
- WHERE
- f.c_id = $courseId AND
- i.c_id = f.c_id AND
- i.tool = 'forum' AND
- f.iid = i.ref AND
- i.visibility = 1";
- $sql = "DELETE FROM $forumCategoryTable
- WHERE c_id = $courseId AND cat_id NOT IN ($subQuery)";
- Database::query($sql);
- }
- /**
- * Deletes all empty link-categories (=without links) from current course.
- * Links are already dealt with by recycle_links() but if recycle is called
- * on categories and not on link, then non-empty categories will survive
- * the recycling.
- */
- public function recycle_link_categories()
- {
- $linkCategoryTable = Database::get_course_table(TABLE_LINK_CATEGORY);
- $linkTable = Database::get_course_table(TABLE_LINK);
- $itemPropertyTable = Database::get_course_table(TABLE_ITEM_PROPERTY);
- $courseId = $this->course_id;
- // c_link.category_id points to c_link_category.id and
- // has to be queried *with* the c_id to ensure a match
- $subQuery = "SELECT distinct(l.category_id) as categoryId
- FROM $linkTable l, $itemPropertyTable i
- WHERE
- l.c_id = $courseId AND
- i.c_id = l.c_id AND
- i.tool = 'link' AND
- l.iid = i.ref AND
- i.visibility = 1";
- $sql = "DELETE FROM $linkCategoryTable
- WHERE c_id = $courseId AND id NOT IN ($subQuery)";
- Database::query($sql);
+ return $qb->getQuery()->getResult();
}
/**
- * Delete events.
+ * Hard-deletes a list of resources. If repository doesn't provide hardDelete(),
+ * falls back to EM->remove() and a final flush (expect proper cascade mappings).
*/
- public function recycle_events()
+ private function hardDeleteMany(string $entityClass, array $resources): void
{
- if ($this->course->has_resources(RESOURCE_EVENT)) {
- $table = Database::get_course_table(TABLE_AGENDA);
- $table_attachment = Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
-
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_EVENT])));
- if (!empty($ids)) {
- $sql = 'DELETE FROM '.$table.'
- WHERE c_id = '.$this->course_id.' AND id IN('.$ids.')';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_attachment.'
- WHERE c_id = '.$this->course_id.' AND agenda_id IN('.$ids.')';
- Database::query($sql);
+ $repo = $this->em->getRepository($entityClass);
+
+ $usedFallback = false;
+ foreach ($resources as $res) {
+ if (method_exists($repo, 'hardDelete')) {
+ // hardDelete takes care of Resource, ResourceNode, Links and Files (Flysystem)
+ $repo->hardDelete($res);
+ } else {
+ // Fallback: standard remove. Ensure your mappings cascade what you need.
+ $this->em->remove($res);
+ $usedFallback = true;
}
}
- }
- /**
- * Delete announcements.
- */
- public function recycle_announcements()
- {
- if ($this->course->has_resources(RESOURCE_ANNOUNCEMENT)) {
- $table = Database::get_course_table(TABLE_ANNOUNCEMENT);
- $table_attachment = Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT);
-
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_ANNOUNCEMENT])));
- if (!empty($ids)) {
- $sql = 'DELETE FROM '.$table.'
- WHERE c_id = '.$this->course_id.' AND id IN('.$ids.')';
- Database::query($sql);
-
- $sql = 'DELETE FROM '.$table_attachment.'
- WHERE c_id = '.$this->course_id.' AND announcement_id IN('.$ids.')';
- Database::query($sql);
- }
+ // One flush at the end. If hardDelete() already flushed internally, this is harmless.
+ if ($usedFallback) {
+ $this->em->flush();
}
}
- /**
- * Recycle quizzes - doesn't remove the questions and their answers,
- * as they might still be used later.
- */
- public function recycle_quizzes()
+ /** Deletes all resources of a type in the course */
+ private function deleteAllOfTypeForCourse(string $entityClass): void
{
- if ($this->course->has_resources(RESOURCE_QUIZ)) {
- $table_qui_que = Database::get_course_table(TABLE_QUIZ_QUESTION);
- $table_qui_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
- $table_qui = Database::get_course_table(TABLE_QUIZ_TEST);
- $table_rel = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
- $table_qui_que_opt = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
- $table_qui_que_cat = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
- $table_qui_que_rel_cat = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
-
- $ids = array_keys($this->course->resources[RESOURCE_QUIZ]);
- // If the value "-1" is in the ids of elements (questions) to
- // be deleted, then consider all orphan questions should be deleted
- // This value is set in CourseBuilder::quiz_build_questions()
- $delete_orphan_questions = in_array(-1, $ids);
- $ids = implode(',', $ids);
-
- if (!empty($ids)) {
- // Deletion of the tests first. Questions in these tests are
- // not deleted and become orphan at this point
- $sql = 'DELETE FROM '.$table_qui.'
- WHERE c_id = '.$this->course_id.' AND id IN('.$ids.')';
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_rel.'
- WHERE c_id = '.$this->course_id.' AND quiz_id IN('.$ids.')';
- Database::query($sql);
- }
-
- // Identifying again and deletion of the orphan questions, if it was desired.
- if ($delete_orphan_questions) {
- // If this query was ever too slow, there is an alternative here:
- // https://github.com/beeznest/chamilo-lms-icpna/commit/a38eab725402188dffff50245ee068d79bcef779
- $sql = " (
- SELECT q.id, ex.c_id FROM $table_qui_que q
- INNER JOIN $table_rel r
- ON (q.c_id = r.c_id AND q.id = r.question_id)
-
- INNER JOIN $table_qui ex
- ON (ex.id = r.quiz_id AND ex.c_id = r.c_id)
- WHERE ex.c_id = ".$this->course_id." AND (ex.active = '-1' OR ex.id = '-1')
- )
- UNION
- (
- SELECT q.id, r.c_id FROM $table_qui_que q
- LEFT OUTER JOIN $table_rel r
- ON (q.c_id = r.c_id AND q.id = r.question_id)
- WHERE q.c_id = ".$this->course_id." AND r.question_id is null
- )
- UNION
- (
- SELECT q.id, r.c_id FROM $table_qui_que q
- INNER JOIN $table_rel r
- ON (q.c_id = r.c_id AND q.id = r.question_id)
- WHERE r.c_id = ".$this->course_id." AND (r.quiz_id = '-1' OR r.quiz_id = '0')
- )";
- $db_result = Database::query($sql);
- if (Database::num_rows($db_result) > 0) {
- $orphan_ids = [];
- while ($obj = Database::fetch_object($db_result)) {
- $orphan_ids[] = $obj->id;
- }
- $orphan_ids = implode(',', $orphan_ids);
- $sql = 'DELETE FROM '.$table_rel.'
- WHERE c_id = '.$this->course_id.' AND question_id IN('.$orphan_ids.')';
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_qui_ans.'
- WHERE c_id = '.$this->course_id.' AND question_id IN('.$orphan_ids.')';
- Database::query($sql);
- $sql = 'DELETE FROM '.$table_qui_que.'
- WHERE c_id = '.$this->course_id.' AND id IN('.$orphan_ids.')';
- Database::query($sql);
- }
- // Also delete questions categories and options
- $sql = "DELETE FROM $table_qui_que_rel_cat WHERE c_id = ".$this->course_id;
- Database::query($sql);
- $sql = "DELETE FROM $table_qui_que_cat WHERE c_id = ".$this->course_id;
- Database::query($sql);
- $sql = "DELETE FROM $table_qui_que_opt WHERE c_id = ".$this->course_id;
- Database::query($sql);
- }
-
- // Quizzes previously deleted are, in fact, kept with a status
- // (active field) of "-1". Delete those, now.
- $sql = 'DELETE FROM '.$table_qui.' WHERE c_id = '.$this->course_id.' AND active = -1';
- Database::query($sql);
+ $resources = $this->fetchResourcesForCourse($entityClass, null);
+ if ($resources) {
+ $this->hardDeleteMany($entityClass, $resources);
}
}
- /**
- * Recycle tests categories.
- */
- public function recycle_test_category()
+ /** Deletes selected resources (by iid) of a type in the course */
+ private function deleteSelectedOfTypeForCourse(string $entityClass, array $ids): void
{
- if (isset($this->course->resources[RESOURCE_TEST_CATEGORY])) {
- foreach ($this->course->resources[RESOURCE_TEST_CATEGORY] as $tab_test_cat) {
- $obj_cat = new TestCategory();
- $obj_cat->removeCategory($tab_test_cat->source_id);
- }
+ if (!$ids) {
+ return;
+ }
+ $resources = $this->fetchResourcesForCourse($entityClass, $ids);
+ if ($resources) {
+ $this->hardDeleteMany($entityClass, $resources);
}
}
- /**
- * Recycle surveys - removes everything.
- */
- public function recycle_surveys()
+ /** Optional post-clean for empty categories if repository supports it */
+ private function autoCleanIfSupported(string $entityClass): void
{
- if ($this->course->has_resources(RESOURCE_SURVEY)) {
- $table_survey = Database::get_course_table(TABLE_SURVEY);
- $table_survey_q = Database::get_course_table(TABLE_SURVEY_QUESTION);
- $table_survey_q_o = Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION);
- $table_survey_a = Database::get_course_Table(TABLE_SURVEY_ANSWER);
- $table_survey_i = Database::get_course_table(TABLE_SURVEY_INVITATION);
- $sql = "DELETE FROM $table_survey_i
- WHERE c_id = ".$this->course_id;
- Database::query($sql);
-
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_SURVEY])));
- if (!empty($ids)) {
- $sql = "DELETE FROM $table_survey_a
- WHERE c_id = ".$this->course_id.' AND survey_id IN('.$ids.')';
- Database::query($sql);
-
- $sql = "DELETE FROM $table_survey_q_o
- WHERE c_id = ".$this->course_id.' AND survey_id IN('.$ids.')';
- Database::query($sql);
-
- $sql = "DELETE FROM $table_survey_q
- WHERE c_id = ".$this->course_id.' AND survey_id IN('.$ids.')';
- Database::query($sql);
-
- $sql = "DELETE FROM $table_survey
- WHERE c_id = ".$this->course_id.' AND survey_id IN('.$ids.')';
- Database::query($sql);
- }
+ $repo = $this->em->getRepository($entityClass);
+ if (method_exists($repo, 'deleteEmptyByCourse')) {
+ $repo->deleteEmptyByCourse($this->courseId);
}
}
- /**
- * Recycle learning paths.
- */
- public function recycle_learnpaths()
+ /** Detach categories from ALL LPs in course (repo-level bulk method preferred if available) */
+ private function clearLpCategoriesForCourse(): void
{
- if ($this->course->has_resources(RESOURCE_LEARNPATH)) {
- $table_main = Database::get_course_table(TABLE_LP_MAIN);
- $table_item = Database::get_course_table(TABLE_LP_ITEM);
- $table_view = Database::get_course_table(TABLE_LP_VIEW);
- $table_iv = Database::get_course_table(TABLE_LP_ITEM_VIEW);
- $table_iv_int = Database::get_course_table(TABLE_LP_IV_INTERACTION);
- $table_tool = Database::get_course_table(TABLE_TOOL_LIST);
-
- foreach ($this->course->resources[RESOURCE_LEARNPATH] as $id => $learnpath) {
- // See task #875.
- if (2 == $learnpath->lp_type) {
- // This is a learning path of SCORM type.
- // A sanity check for avoiding removal of the parent folder scorm/
- if ('' != trim($learnpath->path)) {
- // when $learnpath->path value is incorrect for some reason.
- // The directory trat contains files of the SCORM package is to be deleted.
- $scorm_package_dir = realpath($this->course->path.'scorm/'.$learnpath->path);
- rmdirr($scorm_package_dir);
- }
- }
-
- //remove links from course homepage
- $sql = "DELETE FROM $table_tool
- WHERE
- c_id = ".$this->course_id." AND
- link LIKE '%lp_controller.php%lp_id=$id%' AND
- image='scormbuilder.gif'";
- Database::query($sql);
- //remove elements from lp_* tables (from bottom-up)
- // by removing interactions, then item_view, then views and items, then paths
- $sql_items = "SELECT id FROM $table_item
- WHERE c_id = ".$this->course_id." AND lp_id=$id";
- $res_items = Database::query($sql_items);
- while ($row_item = Database::fetch_array($res_items)) {
- //get item views
- $sql_iv = "SELECT id FROM $table_iv
- WHERE c_id = ".$this->course_id.' AND lp_item_id='.$row_item['id'];
- $res_iv = Database::query($sql_iv);
- while ($row_iv = Database::fetch_array($res_iv)) {
- //delete interactions
- $sql_iv_int_del = "DELETE FROM $table_iv_int
- WHERE c_id = ".$this->course_id.' AND lp_iv_id = '.$row_iv['id'];
- Database::query($sql_iv_int_del);
- }
- //delete item views
- $sql_iv_del = "DELETE FROM $table_iv
- WHERE c_id = ".$this->course_id.' AND lp_item_id='.$row_item['id'];
- Database::query($sql_iv_del);
+ $lps = $this->fetchResourcesForCourse(CLp::class, null);
+ $changed = false;
+ foreach ($lps as $lp) {
+ if (method_exists($lp, 'getCategory') && method_exists($lp, 'setCategory')) {
+ if ($lp->getCategory()) {
+ $lp->setCategory(null);
+ $this->em->persist($lp);
+ $changed = true;
}
- //delete items
- $sql_items_del = "DELETE FROM $table_item WHERE c_id = ".$this->course_id." AND lp_id=$id";
- Database::query($sql_items_del);
- //delete views
- $sql_views_del = "DELETE FROM $table_view WHERE c_id = ".$this->course_id." AND lp_id=$id";
- Database::query($sql_views_del);
- //delete lps
- $sql_del = "DELETE FROM $table_main WHERE c_id = ".$this->course_id." AND id = $id";
- Database::query($sql_del);
}
}
- }
-
- /**
- * Recycle selected learning path categories and dissociate learning paths
- * that are associated with it.
- */
- public function recycle_learnpath_categories()
- {
- $learningPathTable = Database::get_course_table(TABLE_LP_MAIN);
- $learningPathCategoryTable = Database::get_course_table(TABLE_LP_CATEGORY);
- $tblCTool = Database::get_course_table(TABLE_TOOL_LIST);
- if (isset($this->course->resources[RESOURCE_LEARNPATH_CATEGORY])) {
- foreach ($this->course->resources[RESOURCE_LEARNPATH_CATEGORY] as $id => $learnpathCategory) {
- $categoryId = $learnpathCategory->object->getId();
- $sql = "DELETE FROM $tblCTool WHERE c_id = {$this->course_id}
- AND link LIKE '%lp_controller.php%action=view_category&id=$categoryId%'";
- Database::query($sql);
- // Dissociate learning paths from categories that will be deleted
- $sql = "UPDATE $learningPathTable SET category_id = 0 WHERE category_id = ".$categoryId;
- Database::query($sql);
- $sql = "DELETE FROM $learningPathCategoryTable WHERE iid = ".$categoryId;
- Database::query($sql);
- }
+ if ($changed) {
+ $this->em->flush();
}
}
- /**
- * Delete course description.
- */
- public function recycle_cours_description()
+ /** Detach categories only for LPs that are linked to given category ids */
+ private function clearLpCategoriesForIds(array $catIds): void
{
- if ($this->course->has_resources(RESOURCE_COURSEDESCRIPTION)) {
- $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
- $ids = implode(',', array_filter(array_keys($this->course->resources[RESOURCE_COURSEDESCRIPTION])));
- if (!empty($ids)) {
- $sql = "DELETE FROM $table
- WHERE c_id = ".$this->course_id.' AND id IN('.$ids.')';
- Database::query($sql);
+ $lps = $this->fetchResourcesForCourse(CLp::class, null);
+ $changed = false;
+ foreach ($lps as $lp) {
+ $cat = method_exists($lp, 'getCategory') ? $lp->getCategory() : null;
+ $catId = $cat?->getId();
+ if ($catId !== null && \in_array($catId, $catIds, true) && method_exists($lp, 'setCategory')) {
+ $lp->setCategory(null);
+ $this->em->persist($lp);
+ $changed = true;
}
}
- }
-
- /**
- * Recycle Thematics.
- */
- public function recycle_thematic($session_id = 0)
- {
- if ($this->course->has_resources(RESOURCE_THEMATIC)) {
- $table_thematic = Database::get_course_table(TABLE_THEMATIC);
- $table_thematic_advance = Database::get_course_table(TABLE_THEMATIC_ADVANCE);
- $table_thematic_plan = Database::get_course_table(TABLE_THEMATIC_PLAN);
-
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_THEMATIC] as $last_id => $thematic) {
- if (is_numeric($last_id)) {
- foreach ($thematic->thematic_advance_list as $thematic_advance) {
- $cond = [
- 'id = ? AND c_id = ?' => [
- $thematic_advance['id'],
- $this->course_id,
- ],
- ];
- api_item_property_update(
- $this->course_info,
- 'thematic_advance',
- $thematic_advance['id'],
- 'ThematicAdvanceDeleted',
- api_get_user_id()
- );
- Database::delete($table_thematic_advance, $cond);
- }
-
- foreach ($thematic->thematic_plan_list as $thematic_plan) {
- $cond = [
- 'id = ? AND c_id = ?' => [
- $thematic_plan['id'],
- $this->course_id,
- ],
- ];
- api_item_property_update(
- $this->course_info,
- 'thematic_plan',
- $thematic_advance['id'],
- 'ThematicPlanDeleted',
- api_get_user_id()
- );
- Database::delete($table_thematic_plan, $cond);
- }
- $cond = [
- 'id = ? AND c_id = ?' => [
- $last_id,
- $this->course_id,
- ],
- ];
- api_item_property_update(
- $this->course_info,
- 'thematic',
- $last_id,
- 'ThematicDeleted',
- api_get_user_id()
- );
- Database::delete($table_thematic, $cond);
- }
- }
+ if ($changed) {
+ $this->em->flush();
}
}
- /**
- * Recycle Attendances.
- */
- public function recycle_attendance($session_id = 0)
+ /** SCORM directory cleanup for ALL LPs (hook your storage service here if needed) */
+ private function cleanupScormDirsForAllLp(): void
{
- if ($this->course->has_resources(RESOURCE_ATTENDANCE)) {
- $table_attendance = Database::get_course_table(TABLE_ATTENDANCE);
- $table_attendance_calendar = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR);
-
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_ATTENDANCE] as $last_id => $obj) {
- if (is_numeric($last_id)) {
- foreach ($obj->attendance_calendar as $attendance_calendar) {
- $cond = ['id = ? AND c_id = ? ' => [$attendance_calendar['id'], $this->course_id]];
- Database::delete($table_attendance_calendar, $cond);
- }
- $cond = ['id = ? AND c_id = ?' => [$last_id, $this->course_id]];
- Database::delete($table_attendance, $cond);
- api_item_property_update(
- $this->course_info,
- TOOL_ATTENDANCE,
- $last_id,
- 'AttendanceDeleted',
- api_get_user_id()
- );
- }
- }
- }
+ // If you have a storage/scorm service, invoke it here.
+ // By default, nothing: hardDelete already deletes files linked to ResourceNode.
}
- /**
- * Recycle Works.
- */
- public function recycle_work($session_id = 0)
+ /** SCORM directory cleanup for selected LPs */
+ private function cleanupScormDirsForLpIds(array $lpIds): void
{
- if ($this->course->has_resources(RESOURCE_WORK)) {
- $table_work = Database::get_course_table(TABLE_STUDENT_PUBLICATION);
- $table_work_assignment = Database::get_course_table(TABLE_STUDENT_PUBLICATION_ASSIGNMENT);
-
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_WORK] as $last_id => $obj) {
- if (is_numeric($last_id)) {
- $cond = ['publication_id = ? AND c_id = ? ' => [$last_id, $this->course_id]];
- Database::delete($table_work_assignment, $cond);
- // The following also deletes student tasks
- $cond = ['parent_id = ? AND c_id = ?' => [$last_id, $this->course_id]];
- Database::delete($table_work, $cond);
- // Finally, delete the main task registry
- $cond = ['id = ? AND c_id = ?' => [$last_id, $this->course_id]];
- Database::delete($table_work, $cond);
- api_item_property_update(
- $this->course_info,
- TOOL_STUDENTPUBLICATION,
- $last_id,
- 'StudentPublicationDeleted',
- api_get_user_id()
- );
- }
- }
- }
+ // Same as above, but limited to provided LP ids.
}
}
diff --git a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php
index 675d0bbd509..6e8a71338f2 100644
--- a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php
+++ b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php
@@ -4,25 +4,72 @@
namespace Chamilo\CourseBundle\Component\CourseCopy;
-use AbstractLink;
-use Category;
-use Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup;
+use AllowDynamicProperties;
+use Chamilo\CoreBundle\Entity\GradebookCategory;
+use Chamilo\CoreBundle\Entity\GradebookEvaluation;
+use Chamilo\CoreBundle\Entity\GradebookLink;
+use Chamilo\CoreBundle\Entity\GradeModel;
+use Chamilo\CoreBundle\Entity\ResourceLink;
+use Chamilo\CoreBundle\Entity\Room;
+use Chamilo\CoreBundle\Entity\Session as SessionEntity;
+use Chamilo\CoreBundle\Entity\Course as CourseEntity;
+use Chamilo\CoreBundle\Entity\Tool;
+use Chamilo\CoreBundle\Framework\Container;
+use Chamilo\CoreBundle\Helpers\ChamiloHelper;
+use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
+use Chamilo\CoreBundle\Tool\User;
use Chamilo\CourseBundle\Component\CourseCopy\Resources\LearnPathCategory;
-use Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestion;
+use Chamilo\CourseBundle\Entity\CAnnouncement;
+use Chamilo\CourseBundle\Entity\CAnnouncementAttachment;
+use Chamilo\CourseBundle\Entity\CAttendance;
+use Chamilo\CourseBundle\Entity\CAttendanceCalendar;
+use Chamilo\CourseBundle\Entity\CAttendanceCalendarRelGroup;
+use Chamilo\CourseBundle\Entity\CCalendarEvent;
+use Chamilo\CourseBundle\Entity\CCalendarEventAttachment;
+use Chamilo\CourseBundle\Entity\CCourseDescription;
+use Chamilo\CourseBundle\Entity\CDocument;
+use Chamilo\CourseBundle\Entity\CForum;
+use Chamilo\CourseBundle\Entity\CForumCategory;
+use Chamilo\CourseBundle\Entity\CForumPost;
+use Chamilo\CourseBundle\Entity\CForumThread;
+use Chamilo\CourseBundle\Entity\CGlossary;
+use Chamilo\CourseBundle\Entity\CLink;
+use Chamilo\CourseBundle\Entity\CLinkCategory;
+use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpCategory;
+use Chamilo\CourseBundle\Entity\CLpItem;
+use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\CourseBundle\Entity\CQuizAnswer;
+use Chamilo\CourseBundle\Entity\CQuizQuestion;
+use Chamilo\CourseBundle\Entity\CQuizQuestionOption;
+use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
+use Chamilo\CourseBundle\Entity\CStudentPublication;
+use Chamilo\CourseBundle\Entity\CStudentPublicationAssignment;
+use Chamilo\CourseBundle\Entity\CSurvey;
+use Chamilo\CourseBundle\Entity\CSurveyQuestion;
+use Chamilo\CourseBundle\Entity\CSurveyQuestionOption;
+use Chamilo\CourseBundle\Entity\CThematic;
+use Chamilo\CourseBundle\Entity\CThematicAdvance;
+use Chamilo\CourseBundle\Entity\CThematicPlan;
+use Chamilo\CourseBundle\Entity\CTool;
+use Chamilo\CourseBundle\Entity\CToolIntro;
+use Chamilo\CourseBundle\Entity\CWiki;
+use Chamilo\CourseBundle\Entity\CWikiConf;
+use Chamilo\CourseBundle\Repository\CGlossaryRepository;
+use Chamilo\CourseBundle\Repository\CStudentPublicationRepository;
+use Chamilo\CourseBundle\Repository\CWikiRepository;
use CourseManager;
use Database;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\EntityManagerInterface;
use DocumentManager;
-use Evaluation;
-use ExtraFieldValue;
use GroupManager;
-use Image;
use learnpath;
-use Question;
-use stdClass;
-use SurveyManager;
-use TestCategory;
+use PhpZip\ZipFile;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+use Symfony\Component\HttpKernel\KernelInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RouterInterface;
/**
* Class CourseRestorer.
@@ -32,30 +79,33 @@
* @author Bart Mollet
* @author Julio Montoya Several fixes/improvements
*/
+#[AllowDynamicProperties]
class CourseRestorer
{
+ /** Debug flag (default: true). Toggle with setDebug(). */
+ private bool $debug = true;
+
/**
* The course-object.
*/
public $course;
public $destination_course_info;
- /**
- * What to do with files with same name (FILE_SKIP, FILE_RENAME or
- * FILE_OVERWRITE).
- */
+ /** What to do with files with same name (FILE_SKIP, FILE_RENAME, FILE_OVERWRITE). */
public $file_option;
public $set_tools_invisible_by_default;
public $skip_content;
+
+ /** Restore order (keep existing order; docs first). */
public $tools_to_restore = [
- 'documents', // first restore documents
+ 'documents',
'announcements',
'attendance',
'course_descriptions',
'events',
'forum_category',
'forums',
- // 'forum_topics',
+ // 'forum_topics',
'glossary',
'quizzes',
'test_category',
@@ -64,7 +114,7 @@ class CourseRestorer
'surveys',
'learnpath_category',
'learnpaths',
- //'scorm_documents', ??
+ 'scorm_documents',
'tool_intro',
'thematic',
'wiki',
@@ -75,13 +125,52 @@ class CourseRestorer
/** Setting per tool */
public $tool_copy_settings = [];
- /**
- * If true adds the text "copy" in the title of an item (only for LPs right now).
- */
+ /** If true adds the text "copy" in the title of an item (only for LPs right now). */
public $add_text_in_items = false;
+
public $destination_course_id;
public bool $copySessionContent = false;
+ /** Optional course origin id (legacy). */
+ private $course_origin_id = null;
+
+ /** First teacher (owner) used for forums/posts. */
+ private $first_teacher_id = 0;
+
+ /** Destination course entity cache. */
+ private $destination_course_entity;
+
+ /**
+ * Centralized logger controlled by $this->debug.
+ */
+ private function dlog(string $message, array $context = []): void
+ {
+ if (!$this->debug) {
+ return;
+ }
+ $ctx = '';
+ if (!empty($context)) {
+ try {
+ $ctx = ' ' . json_encode(
+ $context,
+ JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR
+ );
+ } catch (\Throwable $e) {
+ $ctx = ' [context_json_failed: '.$e->getMessage().']';
+ }
+ }
+ error_log('COURSE_DEBUG: '.$message.$ctx);
+ }
+
+ /**
+ * Public setter for the debug flag.
+ */
+ public function setDebug(?bool $on = true): void
+ {
+ $this->debug = (bool) $on;
+ $this->dlog('Debug flag changed', ['debug' => $this->debug]);
+ }
+
/**
* CourseRestorer constructor.
*
@@ -89,31 +178,36 @@ class CourseRestorer
*/
public function __construct($course)
{
+ // Read env constant/course hint if present
+ if (defined('COURSE_RESTORER_DEBUG')) {
+ $this->debug = (bool) constant('COURSE_RESTORER_DEBUG');
+ }
+
$this->course = $course;
$courseInfo = api_get_course_info($this->course->code);
- $this->course_origin_id = null;
- if (!empty($courseInfo)) {
- $this->course_origin_id = $courseInfo['real_id'];
- }
+ $this->course_origin_id = !empty($courseInfo) ? $courseInfo['real_id'] : null;
+
$this->file_option = FILE_RENAME;
$this->set_tools_invisible_by_default = false;
$this->skip_content = [];
- $forceImport = ('true' === api_get_setting('lp.allow_import_scorm_package_in_course_builder'));
- if ($forceImport) {
- $this->tools_to_restore[] = 'scorm_documents';
- }
+ $this->dlog('Ctor: initial course info', [
+ 'course_code' => $this->course->code ?? null,
+ 'origin_id' => $this->course_origin_id,
+ 'has_resources' => is_array($this->course->resources ?? null),
+ 'resource_keys' => array_keys((array) ($this->course->resources ?? [])),
+ ]);
}
/**
* Set the file-option.
*
- * @param int $option (optional) What to do with files with same name
- * FILE_SKIP, FILE_RENAME or FILE_OVERWRITE
+ * @param int $option FILE_SKIP, FILE_RENAME or FILE_OVERWRITE
*/
public function set_file_option($option = FILE_OVERWRITE)
{
$this->file_option = $option;
+ $this->dlog('File option set', ['file_option' => $this->file_option]);
}
/**
@@ -132,15 +226,65 @@ public function set_tool_copy_settings($array)
$this->tool_copy_settings = $array;
}
+ /** Normalize forum keys so internal bags are always available. */
+ private function normalizeForumKeys(): void
+ {
+ if (!is_array($this->course->resources ?? null)) {
+ $this->course->resources = [];
+ return;
+ }
+ $r = $this->course->resources;
+
+ // Categories
+ if (!isset($r['Forum_Category']) && isset($r['forum_category'])) {
+ $r['Forum_Category'] = $r['forum_category'];
+ }
+
+ // Forums
+ if (!isset($r['forum']) && isset($r['Forum'])) {
+ $r['forum'] = $r['Forum'];
+ }
+
+ // Topics
+ if (!isset($r['thread']) && isset($r['forum_topic'])) {
+ $r['thread'] = $r['forum_topic'];
+ } elseif (!isset($r['thread']) && isset($r['Forum_Thread'])) {
+ $r['thread'] = $r['Forum_Thread'];
+ }
+
+ // Posts
+ if (!isset($r['post']) && isset($r['forum_post'])) {
+ $r['post'] = $r['forum_post'];
+ } elseif (!isset($r['post']) && isset($r['Forum_Post'])) {
+ $r['post'] = $r['Forum_Post'];
+ }
+
+ $this->course->resources = $r;
+ $this->dlog('Forum keys normalized', [
+ 'has_Forum_Category' => isset($r['Forum_Category']),
+ 'forum_count' => isset($r['forum']) && is_array($r['forum']) ? count($r['forum']) : 0,
+ 'thread_count' => isset($r['thread']) && is_array($r['thread']) ? count($r['thread']) : 0,
+ 'post_count' => isset($r['post']) && is_array($r['post']) ? count($r['post']) : 0,
+ ]);
+ }
+
+ private function resetDoctrineIfClosed(): void
+ {
+ try {
+ $em = \Database::getManager();
+ if (!$em->isOpen()) {
+ $registry = Container::$container->get('doctrine');
+ $registry->resetManager();
+ } else {
+ $em->clear();
+ }
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: resetDoctrineIfClosed failed: '.$e->getMessage());
+ }
+ }
+
/**
- * Restore a course.
- *
- * @param string $destination_course_code code of the Chamilo-course in
- * @param int $session_id
- * @param bool $update_course_settings Course settings are going to be restore?
- * @param bool $respect_base_content
- *
- * @return false|null
+ * Entry point.
*/
public function restore(
$destination_course_code = '',
@@ -148,2994 +292,3343 @@ public function restore(
$update_course_settings = false,
$respect_base_content = false
) {
- if ('' == $destination_course_code) {
- $course_info = api_get_course_info();
- $this->destination_course_info = $course_info;
- $this->course->destination_path = $course_info['path'];
- } else {
- $course_info = api_get_course_info($destination_course_code);
- $this->destination_course_info = $course_info;
- $this->course->destination_path = $course_info['path'];
+ $this->dlog('Restore() called', [
+ 'destination_code' => $destination_course_code,
+ 'session_id' => (int) $session_id,
+ 'update_course_settings' => (bool) $update_course_settings,
+ 'respect_base_content' => (bool) $respect_base_content,
+ ]);
+
+ // Resolve destination course
+ $course_info = $destination_course_code === ''
+ ? api_get_course_info()
+ : api_get_course_info($destination_course_code);
+
+ if (empty($course_info) || empty($course_info['real_id'])) {
+ $this->dlog('Destination course not resolved or missing real_id', ['course_info' => $course_info]);
+ return false;
}
- $this->destination_course_id = $course_info['real_id'];
- // Getting first teacher (for the forums)
- $teacher_list = CourseManager::get_teacher_list_from_course_code($course_info['code']);
- $this->first_teacher_id = api_get_user_id();
+ $this->destination_course_info = $course_info;
+ $this->destination_course_id = (int) $course_info['real_id'];
+ $this->destination_course_entity = api_get_course_entity($this->destination_course_id);
+ // Resolve teacher for forum/thread/post ownership
+ $this->first_teacher_id = api_get_user_id();
+ $teacher_list = CourseManager::get_teacher_list_from_course_code($course_info['code']);
if (!empty($teacher_list)) {
- foreach ($teacher_list as $teacher) {
- $this->first_teacher_id = $teacher['user_id'];
-
- break;
- }
+ foreach ($teacher_list as $t) { $this->first_teacher_id = (int) $t['user_id']; break; }
}
if (empty($this->course)) {
+ $this->dlog('No source course found');
return false;
}
- // Source platform encoding - reading/detection
- // The correspondent data field has been added as of version 1.8.6.1
+ // Encoding detection/normalization
if (empty($this->course->encoding)) {
- // The archive has been created by a system which is prior to 1.8.6.1 version.
- // In this case we have to detect the encoding.
$sample_text = $this->course->get_sample_text()."\n";
- // Let us exclude ASCII lines, probably they are English texts.
- $sample_text = explode("\n", $sample_text);
- foreach ($sample_text as $key => &$line) {
- if (api_is_valid_ascii($line)) {
- unset($sample_text[$key]);
- }
+ $lines = explode("\n", $sample_text);
+ foreach ($lines as $k => $line) {
+ if (api_is_valid_ascii($line)) { unset($lines[$k]); }
}
- $sample_text = implode("\n", $sample_text);
- $this->course->encoding = api_detect_encoding(
- $sample_text,
- $course_info['language']
- );
+ $sample_text = implode("\n", $lines);
+ $this->course->encoding = api_detect_encoding($sample_text, $course_info['language']);
}
-
- // Encoding conversion of the course, if it is needed.
$this->course->to_system_encoding();
+ $this->dlog('Encoding resolved', ['encoding' => $this->course->encoding ?? '']);
+
+ // Normalize forum bags
+ $this->normalizeForumKeys();
+ // Dump a compact view of the resource bags before restoring
+ $this->debug_course_resources_simple(null);
+
+ // Restore tools
foreach ($this->tools_to_restore as $tool) {
- $function_build = 'restore_'.$tool;
- $this->$function_build(
- $session_id,
- $respect_base_content,
- $destination_course_code
- );
+ $fn = 'restore_'.$tool;
+ if (method_exists($this, $fn)) {
+ $this->dlog('Starting tool restore', ['tool' => $tool]);
+ try {
+ $this->{$fn}($session_id, $respect_base_content, $destination_course_code);
+ } catch (\Throwable $e) {
+ $this->dlog('Tool restore failed with exception', [
+ 'tool' => $tool,
+ 'error' => $e->getMessage(),
+ ]);
+ $this->resetDoctrineIfClosed();
+ }
+ $this->dlog('Finished tool restore', ['tool' => $tool]);
+ } else {
+ $this->dlog('Restore method not found for tool (skipping)', ['tool' => $tool]);
+ }
}
+ // Optionally restore safe course settings
if ($update_course_settings) {
+ $this->dlog('Restoring course settings');
$this->restore_course_settings($destination_course_code);
}
- // Restore the item properties
- $table = Database::get_course_table(TABLE_ITEM_PROPERTY);
- foreach ($this->course->resources as $type => $resources) {
- if (is_array($resources)) {
- foreach ($resources as $id => $resource) {
- if (isset($resource->item_properties)) {
- foreach ($resource->item_properties as $property) {
- // First check if there isn't already a record for this resource
- $sql = "SELECT * FROM $table
- WHERE
- c_id = ".$this->destination_course_id." AND
- tool = '".$property['tool']."' AND
- ref = '".$resource->destination_id."'";
-
- $params = [];
- if (!empty($session_id)) {
- $params['session_id'] = (int) $session_id;
- }
+ $this->dlog('Restore() finished', [
+ 'destination_course_id' => $this->destination_course_id,
+ ]);
- $res = Database::query($sql);
- if (0 == Database::num_rows($res)) {
- /* The to_group_id and to_user_id are set to default
- values as users/groups possibly not exist in
- the target course*/
-
- $params['c_id'] = $this->destination_course_id;
- $params['tool'] = self::DBUTF8($property['tool']);
- $params['insert_user_id'] = $this->checkUserId($property['insert_user_id']) ?: null;
- $params['insert_date'] = self::DBUTF8($property['insert_date']);
- $params['lastedit_date'] = self::DBUTF8($property['lastedit_date']);
- $params['ref'] = $resource->destination_id;
- $params['lastedit_type'] = self::DBUTF8($property['lastedit_type']);
- $params['lastedit_user_id'] = $this->checkUserId($property['lastedit_user_id']);
- $params['visibility'] = self::DBUTF8($property['visibility']);
- $params['start_visible'] = self::DBUTF8($property['start_visible']);
- $params['end_visible'] = self::DBUTF8($property['end_visible']);
- $params['to_user_id'] = $this->checkUserId($property['to_user_id']) ?: null;
-
- $id = Database::insert($table, $params);
- if ($id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $id";
- Database::query($sql);
- }
- }
- }
- }
+ return null;
+ }
+
+ /**
+ * Restore only harmless course settings (Chamilo 2 entity-safe).
+ */
+ public function restore_course_settings(string $destination_course_code = ''): void
+ {
+ $this->dlog('restore_course_settings() called');
+
+ $courseEntity = null;
+
+ if ($destination_course_code !== '') {
+ $courseEntity = Container::getCourseRepository()->findOneByCode($destination_course_code);
+ } else {
+ if (!empty($this->destination_course_id)) {
+ $courseEntity = api_get_course_entity((int) $this->destination_course_id);
+ } else {
+ $info = api_get_course_info();
+ if (!empty($info['real_id'])) {
+ $courseEntity = api_get_course_entity((int) $info['real_id']);
}
}
}
+
+ if (!$courseEntity) {
+ $this->dlog('No destination course entity found, skipping settings restore');
+ return;
+ }
+
+ $src = $this->course->info ?? [];
+
+ if (!empty($src['language'])) {
+ $courseEntity->setCourseLanguage((string) $src['language']);
+ }
+ if (isset($src['visibility']) && $src['visibility'] !== '') {
+ $courseEntity->setVisibility((int) $src['visibility']);
+ }
+ if (array_key_exists('department_name', $src)) {
+ $courseEntity->setDepartmentName((string) $src['department_name']);
+ }
+ if (array_key_exists('department_url', $src)) {
+ $courseEntity->setDepartmentUrl((string) $src['department_url']);
+ }
+ if (!empty($src['category_id'])) {
+ $catRepo = Container::getCourseCategoryRepository();
+ $cat = $catRepo?->find((int) $src['category_id']);
+ if ($cat) {
+ $courseEntity->setCategories(new ArrayCollection([$cat]));
+ }
+ }
+ if (array_key_exists('subscribe_allowed', $src)) {
+ $courseEntity->setSubscribe((bool) $src['subscribe_allowed']);
+ }
+ if (array_key_exists('unsubscribe', $src)) {
+ $courseEntity->setUnsubscribe((bool) $src['unsubscribe']);
+ }
+
+ $em = Database::getManager();
+ $em->persist($courseEntity);
+ $em->flush();
+
+ $this->dlog('Course settings restored');
}
- /**
- * Restore only harmless course settings:
- * course_language, visibility, department_name,department_url,
- * subscribe, unsubscribe, category_id.
- *
- * @param string $destination_course_code
- */
- public function restore_course_settings($destination_course_code)
+ private function projectUploadBase(): string
+ {
+ /** @var KernelInterface $kernel */
+ $kernel = Container::$container->get('kernel');
+ return rtrim($kernel->getProjectDir(), '/').'/var/upload/resource';
+ }
+
+ private function resourceFileAbsPathFromDocument(CDocument $doc): ?string
{
- $origin_course_info = api_get_course_info($destination_course_code);
- $course_info = $this->course->info;
- $params['course_language'] = $course_info['language'];
- $params['visibility'] = $course_info['visibility'];
- $params['department_name'] = $course_info['department_name'];
- $params['department_url'] = $course_info['department_url'];
- $params['category_id'] = $course_info['category_id'];
- $params['subscribe'] = $course_info['subscribe_allowed'];
- $params['unsubscribe'] = $course_info['unsubscribe'];
- CourseManager::update_attributes($origin_course_info['real_id'], $params);
+ $node = $doc->getResourceNode();
+ if (!$node) return null;
+
+ $file = $node->getFirstResourceFile();
+ if (!$file) return null;
+
+ /** @var ResourceNodeRepository $rnRepo */
+ $rnRepo = Container::$container->get(ResourceNodeRepository::class);
+ $rel = $rnRepo->getFilename($file);
+ if (!$rel) return null;
+
+ $abs = $this->projectUploadBase().$rel;
+ return is_readable($abs) ? $abs : null;
}
/**
* Restore documents.
- *
- * @param int $session_id
- * @param bool $respect_base_content
- * @param string $destination_course_code
*/
- public function restore_documents(
- $session_id = 0,
- $respect_base_content = false,
- $destination_course_code = ''
- ) {
- $course_info = api_get_course_info($destination_course_code);
-
+ public function restore_documents($session_id = 0, $respect_base_content = false, $destination_course_code = '')
+ {
if (!$this->course->has_resources(RESOURCE_DOCUMENT)) {
+ $this->dlog('restore_documents: no document resources');
return;
}
- $webEditorCss = api_get_path(WEB_CSS_PATH).'editor.css';
- $table = Database::get_course_table(TABLE_DOCUMENT);
- $resources = $this->course->resources;
- $path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/';
- $originalFolderNameList = [];
- foreach ($resources[RESOURCE_DOCUMENT] as $id => $document) {
- $my_session_id = empty($document->item_properties[0]['session_id']) ? 0 : $session_id;
- //$path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/';
- if (false === $respect_base_content && $session_id) {
- if (empty($my_session_id)) {
- $my_session_id = $session_id;
- }
- }
+ $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);
- if (FOLDER == $document->file_type) {
- $visibility = isset($document->item_properties[0]['visibility']) ? $document->item_properties[0]['visibility'] : '';
- $new = substr($document->path, 8);
+ $copyMode = empty($this->course->backup_path);
+ $srcRoot = $copyMode ? null : rtrim((string)$this->course->backup_path, '/').'/';
- $folderList = explode('/', $new);
- $tempFolder = '';
+ $this->dlog('restore_documents: begin', [
+ 'files' => count($this->course->resources[RESOURCE_DOCUMENT] ?? []),
+ 'session' => (int) $session_id,
+ 'mode' => $copyMode ? 'copy' : 'import',
+ 'srcRoot' => $srcRoot,
+ ]);
- // Check if the parent path exists.
- foreach ($folderList as $folder) {
- $folderToCreate = $tempFolder.$folder;
- //$sysFolderPath = $path.'document'.$folderToCreate;
- $sysFolderPath = null;
- $tempFolder .= $folder.'/';
+ // 1) folders
+ $folders = [];
+ foreach ($this->course->resources[RESOURCE_DOCUMENT] as $k => $item) {
+ if ($item->file_type !== FOLDER) { continue; }
- if (empty($folderToCreate)) {
- continue;
- }
+ $rel = '/'.ltrim(substr($item->path, 8), '/');
+ if ($rel === '/') { continue; }
- $title = $document->title;
- $originalFolderNameList[basename($document->path)] = $document->title;
- if (empty($title)) {
- $title = basename($sysFolderPath);
- }
+ $parts = array_values(array_filter(explode('/', $rel)));
+ $accum = '';
+ $parentId = 0;
- // File doesn't exist in file system.
- if (!is_dir($sysFolderPath)) {
- // Creating directory
- create_unexisting_directory(
- $course_info,
- api_get_user_id(),
- $my_session_id,
- 0,
- 0,
- $path.'document',
- $folderToCreate,
- $title,
- $visibility
- );
+ foreach ($parts as $i => $seg) {
+ $accum .= '/'.$seg;
+ if (isset($folders[$accum])) { $parentId = $folders[$accum]; continue; }
- continue;
- }
+ $parentResource = $parentId ? $docRepo->find($parentId) : $courseEntity;
+ $title = ($i === count($parts)-1) ? ($item->title ?: $seg) : $seg;
+
+ $existing = $docRepo->findCourseResourceByTitle(
+ $title, $parentResource->getResourceNode(), $courseEntity, $session, $group
+ );
- // File exist in file system.
- $documentData = DocumentManager::get_document_id(
- $course_info,
- $folderToCreate,
- $session_id
+ if ($existing) {
+ $iid = method_exists($existing,'getIid') ? $existing->getIid() : 0;
+ $this->dlog('restore_documents: reuse folder', ['title' => $title, 'iid' => $iid]);
+ } else {
+ $entity = DocumentManager::addDocument(
+ ['real_id'=>$courseInfo['real_id'],'code'=>$courseInfo['code']],
+ $accum, 'folder', 0, $title, null, 0, null, 0, (int)$session_id, 0, false, '', $parentId, ''
);
+ $iid = method_exists($entity,'getIid') ? $entity->getIid() : 0;
+ $this->dlog('restore_documents: created folder', ['title' => $title, 'iid' => $iid]);
+ }
- if (empty($documentData)) {
- /* This means the folder exists in the
- filesystem but not in the DB, trying to fix it */
- DocumentManager::addDocument(
- $course_info,
- $folderToCreate,
- 'folder',
- 0,
- $title,
- null,
- null,
- false,
- null,
- $session_id,
- 0,
- false
- );
- } else {
- $insertUserId = isset($document->item_properties[0]['insert_user_id']) ? $document->item_properties[0]['insert_user_id'] : api_get_user_id();
- $insertUserId = $this->checkUserId($insertUserId);
-
- // Check if user exists in platform
- $toUserId = isset($document->item_properties[0]['to_user_id']) ? $document->item_properties[0]['to_user_id'] : null;
- $toUserId = $this->checkUserId($toUserId, true);
-
- $groupId = isset($document->item_properties[0]['to_group_id']) ? $document->item_properties[0]['to_group_id'] : null;
- $groupInfo = $this->checkGroupId($groupId);
-
- // if folder exists then just refresh it
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $documentData,
- 'FolderUpdated',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- }
+ $folders[$accum] = $iid;
+ if ($i === count($parts)-1) {
+ $this->course->resources[RESOURCE_DOCUMENT][$k]->destination_id = $iid;
}
- } elseif (DOCUMENT == $document->file_type) {
- // Checking if folder exists in the database otherwise we created it
- $dir_to_create = dirname($document->path);
- $originalFolderNameList[basename($document->path)] = $document->title;
- if (!empty($dir_to_create) && 'document' != $dir_to_create && '/' != $dir_to_create) {
- if (is_dir($path.dirname($document->path))) {
- $sql = "SELECT id FROM $table
- WHERE
- c_id = ".$this->destination_course_id." AND
- path = '/".self::DBUTF8escapestring(substr(dirname($document->path), 9))."'";
- $res = Database::query($sql);
-
- if (0 == Database::num_rows($res)) {
- //continue;
- $visibility = $document->item_properties[0]['visibility'];
- $new = '/'.substr(dirname($document->path), 9);
- $title = $document->title;
- if (empty($title)) {
- $title = str_replace('/', '', $new);
- }
+ $parentId = $iid;
+ }
+ }
- // This code fixes the possibility for a file without a directory entry to be
- $document_id = DocumentManager::addDocument(
- $course_info,
- $new,
- 'folder',
- 0,
- $title,
- null,
- null,
- false,
- 0,
- 0,
- 0,
- false
- );
+ // 2) files
+ foreach ($this->course->resources[RESOURCE_DOCUMENT] as $k => $item) {
+ if ($item->file_type !== DOCUMENT) { continue; }
+
+ $srcPath = null;
+ $rawTitle = $item->title ?: basename((string)$item->path);
+ $ext = strtolower(pathinfo($rawTitle, PATHINFO_EXTENSION));
+ $isHtml = in_array($ext, ['html','htm'], true);
+
+ if ($copyMode) {
+ $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 {
+ $srcPath = $srcRoot.$item->path;
+ if (!is_file($srcPath) || !is_readable($srcPath)) {
+ $this->dlog('restore_documents: source file not found/readable', ['src' => $srcPath]);
+ continue;
+ }
+ }
+
+ $rel = '/'.ltrim(substr($item->path, 8), '/');
+ $parentRel = rtrim(dirname($rel), '/');
+ $parentId = $folders[$parentRel] ?? 0;
+ $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity;
+
+ $baseTitle = $rawTitle;
+ $finalTitle = $baseTitle;
+
+ $findExisting = function($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;
+ };
+
+ $existsIid = $findExisting($finalTitle);
+ if ($existsIid) {
+ $this->dlog('restore_documents: collision', ['title' => $finalTitle, 'policy' => $this->file_option]);
+ if ($this->file_option === FILE_SKIP) {
+ $this->course->resources[RESOURCE_DOCUMENT][$k]->destination_id = $existsIid;
+ continue;
+ }
+ $pi = pathinfo($baseTitle);
+ $name = $pi['filename'] ?? $baseTitle;
+ $ext2 = isset($pi['extension']) && $pi['extension'] !== '' ? '.'.$pi['extension'] : '';
+ $i=1;
+ while ($findExisting($finalTitle)) { $finalTitle = $name.'_'.$i.$ext2; $i++; }
+ }
+
+ $content = '';
+ $realPath = '';
+ if ($isHtml) {
+ $raw = @file_get_contents($srcPath) ?: '';
+ if (defined('UTF8_CONVERT') && UTF8_CONVERT) { $raw = utf8_encode($raw); }
+ $content = DocumentManager::replaceUrlWithNewCourseCode(
+ $raw,
+ $this->course->code,
+ $this->course->destination_path,
+ $this->course->backup_path,
+ $this->course->info['path']
+ );
+ } else {
+ $realPath = $srcPath;
+ }
+
+ try {
+ $entity = DocumentManager::addDocument(
+ ['real_id'=>$courseInfo['real_id'],'code'=>$courseInfo['code']],
+ $rel,
+ 'file',
+ (int)($item->size ?? 0),
+ $finalTitle,
+ $item->comment ?? '',
+ 0,
+ null,
+ 0,
+ (int)$session_id,
+ 0,
+ false,
+ $content,
+ $parentId,
+ $realPath
+ );
+ $iid = method_exists($entity,'getIid') ? $entity->getIid() : 0;
+ $this->course->resources[RESOURCE_DOCUMENT][$k]->destination_id = $iid;
+ $this->dlog('restore_documents: file created', [
+ 'title' => $finalTitle,
+ 'iid' => $iid,
+ 'mode' => $copyMode ? 'copy' : 'import'
+ ]);
+ } catch (\Throwable $e) {
+ $this->dlog('restore_documents: file create failed', ['title' => $finalTitle, 'error' => $e->getMessage()]);
+ }
+ }
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
- $groupInfo = $this->checkGroupId($toGroupId);
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $document_id,
- 'FolderCreated',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
+ $this->dlog('restore_documents: end');
+ }
+
+ /**
+ * Compact dump of resources: keys, per-bag counts and one sample (trimmed).
+ */
+ private function debug_course_resources_simple(?string $focusBag = null, int $maxObjFields = 10): void
+ {
+ try {
+ $resources = is_array($this->course->resources ?? null) ? $this->course->resources : [];
+
+ $safe = function ($data): string {
+ try {
+ return json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PARTIAL_OUTPUT_ON_ERROR) ?: '[json_encode_failed]';
+ } catch (\Throwable $e) {
+ return '[json_exception: '.$e->getMessage().']';
+ }
+ };
+ $short = function ($v, int $max = 200) {
+ if (is_string($v)) {
+ $s = trim($v);
+ return mb_strlen($s) > $max ? (mb_substr($s, 0, $max).'…('.mb_strlen($s).' chars)') : $s;
+ }
+ if (is_numeric($v) || is_bool($v) || $v === null) return $v;
+ return '['.gettype($v).']';
+ };
+ $sample = function ($item) use ($short, $maxObjFields) {
+ $out = [
+ 'source_id' => null,
+ 'destination_id' => null,
+ 'type' => null,
+ 'has_obj' => false,
+ 'obj_fields' => [],
+ 'has_item_props' => false,
+ 'extra' => [],
+ ];
+ if (is_object($item) || is_array($item)) {
+ $arr = (array)$item;
+ $out['source_id'] = $arr['source_id'] ?? null;
+ $out['destination_id'] = $arr['destination_id'] ?? null;
+ $out['type'] = $arr['type'] ?? null;
+ $out['has_item_props'] = !empty($arr['item_properties']);
+
+ $obj = $arr['obj'] ?? null;
+ if (is_object($obj) || is_array($obj)) {
+ $out['has_obj'] = true;
+ $objArr = (array)$obj;
+ $fields = [];
+ $i = 0;
+ foreach ($objArr as $k => $v) {
+ if ($i++ >= $maxObjFields) { $fields['__notice'] = 'truncated'; break; }
+ $fields[$k] = $short($v);
}
+ $out['obj_fields'] = $fields;
}
+ foreach (['path','title','comment'] as $k) {
+ if (isset($arr[$k])) $out['extra'][$k] = $short($arr[$k]);
+ }
+ } else {
+ $out['extra']['_type'] = gettype($item);
}
+ return $out;
+ };
- if (file_exists($path.$document->path)) {
- switch ($this->file_option) {
- case FILE_OVERWRITE:
- $origin_path = $this->course->backup_path.'/'.$document->path;
- if (file_exists($origin_path)) {
- copy($origin_path, $path.$document->path);
- $this->fixEditorHtmlContent($path.$document->path, $webEditorCss);
- $sql = "SELECT id FROM $table
- WHERE
- c_id = ".$this->destination_course_id." AND
- path = '/".self::DBUTF8escapestring(substr($document->path, 9))."'";
-
- $res = Database::query($sql);
- $count = Database::num_rows($res);
-
- if (0 == $count) {
- $params = [
- 'path' => '/'.self::DBUTF8(substr($document->path, 9)),
- 'c_id' => $this->destination_course_id,
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($document->title),
- 'filetype' => self::DBUTF8($document->file_type),
- 'size' => self::DBUTF8($document->size),
- 'session_id' => $my_session_id,
- 'readonly' => 0,
- ];
-
- $document_id = Database::insert($table, $params);
-
- if ($document_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $document_id";
- Database::query($sql);
- }
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $document_id;
-
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
-
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
- $groupInfo = $this->checkGroupId($toGroupId);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $document_id,
- 'DocumentAdded',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- } else {
- $obj = Database::fetch_object($res);
- $document_id = $obj->id;
- $params = [
- 'path' => '/'.self::DBUTF8(substr($document->path, 9)),
- 'c_id' => $this->destination_course_id,
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($document->title),
- 'filetype' => self::DBUTF8($document->file_type),
- 'size' => self::DBUTF8($document->size),
- 'session_id' => $my_session_id,
- ];
-
- Database::update(
- $table,
- $params,
- [
- 'c_id = ? AND path = ?' => [
- $this->destination_course_id,
- '/'.self::DBUTF8escapestring(substr($document->path, 9)),
- ],
- ]
- );
+ $this->dlog('Resources overview', ['keys' => array_keys($resources)]);
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $obj->id;
-
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
-
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
- $groupInfo = $this->checkGroupId($toGroupId);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $obj->id,
- 'default',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- }
+ foreach ($resources as $bagName => $bag) {
+ if (!is_array($bag)) {
+ $this->dlog("Bag not an array, skipping", ['bag' => $bagName, 'type' => gettype($bag)]);
+ continue;
+ }
+ $count = count($bag);
+ $this->dlog('Bag count', ['bag' => $bagName, 'count' => $count]);
+
+ if ($count > 0) {
+ $firstKey = array_key_first($bag);
+ $firstVal = $bag[$firstKey];
+ $s = $sample($firstVal);
+ $s['__first_key'] = $firstKey;
+ $s['__class'] = is_object($firstVal) ? get_class($firstVal) : gettype($firstVal);
+ $this->dlog('Bag sample', ['bag' => $bagName, 'sample' => $s]);
+ }
- // Replace old course code with the new destination code
- $file_info = pathinfo($path.$document->path);
+ if ($focusBag !== null && $focusBag === $bagName) {
+ $preview = [];
+ $i = 0;
+ foreach ($bag as $k => $v) {
+ if ($i++ >= 10) { $preview[] = ['__notice' => 'truncated-after-10-items']; break; }
+ $preview[] = ['key' => $k, 'sample' => $sample($v)];
+ }
+ $this->dlog('Bag deep preview', ['bag' => $bagName, 'items' => $preview]);
+ }
+ }
+ } catch (\Throwable $e) {
+ $this->dlog('Failed to dump resources', ['error' => $e->getMessage()]);
+ }
+ }
- if (isset($file_info['extension']) && in_array($file_info['extension'], ['html', 'htm'])) {
- $content = file_get_contents($path.$document->path);
- if (UTF8_CONVERT) {
- $content = utf8_encode($content);
- }
- $content = DocumentManager::replaceUrlWithNewCourseCode(
- $content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- file_put_contents($path.$document->path, $content);
- }
+ public function restore_forum_category($session_id = 0, $respect_base_content = false, $destination_course_code = ''): void
+ {
+ $bag = $this->course->resources['Forum_Category']
+ ?? $this->course->resources['forum_category']
+ ?? [];
- $params = [
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($document->title),
- 'size' => self::DBUTF8($document->size),
- ];
- Database::update(
- $table,
- $params,
- [
- 'c_id = ? AND id = ?' => [
- $this->destination_course_id,
- $document_id,
- ],
- ]
- );
- }
+ if (empty($bag)) {
+ $this->dlog('restore_forum_category: empty bag');
+ return;
+ }
- break;
- case FILE_SKIP:
- $sql = "SELECT id FROM $table
- WHERE
- c_id = ".$this->destination_course_id." AND
- path='/".self::DBUTF8escapestring(substr($document->path, 9))."'";
- $res = Database::query($sql);
- $obj = Database::fetch_object($res);
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $obj->id;
+ $em = Database::getManager();
+ $catRepo = Container::getForumCategoryRepository();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity((int)$session_id);
- break;
- case FILE_RENAME:
- $i = 1;
- $ext = explode('.', basename($document->path));
- if (count($ext) > 1) {
- $ext = array_pop($ext);
- $file_name_no_ext = substr($document->path, 0, -(strlen($ext) + 1));
- $ext = '.'.$ext;
- } else {
- $ext = '';
- $file_name_no_ext = $document->path;
- }
- $new_file_name = $file_name_no_ext.'_'.$i.$ext;
- $file_exists = file_exists($path.$new_file_name);
- while ($file_exists) {
- $i++;
- $new_file_name = $file_name_no_ext.'_'.$i.$ext;
- $file_exists = file_exists($path.$new_file_name);
- }
+ foreach ($bag as $id => $res) {
+ if (!empty($res->destination_id)) { continue; }
- if (!empty($session_id)) {
- $originalPath = $document->path;
- $document_path = explode('/', $document->path, 3);
- $course_path = $path;
- $orig_base_folder = $document_path[1];
- $orig_base_path = $course_path.$document_path[0].'/'.$document_path[1];
-
- if (is_dir($orig_base_path)) {
- $new_base_foldername = $orig_base_folder;
- $new_base_path = $orig_base_path;
-
- if (isset($_SESSION['orig_base_foldername']) &&
- $_SESSION['orig_base_foldername'] != $new_base_foldername
- ) {
- unset($_SESSION['new_base_foldername']);
- unset($_SESSION['orig_base_foldername']);
- unset($_SESSION['new_base_path']);
- }
+ $obj = is_object($res->obj ?? null) ? $res->obj : (object)[];
+ $title = (string)($obj->cat_title ?? $obj->title ?? "Forum category #$id");
+ $comment = (string)($obj->cat_comment ?? $obj->description ?? '');
- $folder_exists = file_exists($new_base_path);
- if ($folder_exists) {
- // e.g: carpeta1 in session
- $_SESSION['orig_base_foldername'] = $new_base_foldername;
- $x = 0;
- while ($folder_exists) {
- $x++;
- $new_base_foldername = $document_path[1].'_'.$x;
- $new_base_path = $orig_base_path.'_'.$x;
- if (isset($_SESSION['new_base_foldername'])
- && $_SESSION['new_base_foldername'] == $new_base_foldername
- ) {
- break;
- }
- $folder_exists = file_exists($new_base_path);
- }
- $_SESSION['new_base_foldername'] = $new_base_foldername;
- $_SESSION['new_base_path'] = $new_base_path;
- }
+ $existing = $catRepo->findOneBy(['title' => $title, 'resourceNode.parent' => $course->getResourceNode()]);
+ if ($existing) {
+ $destIid = (int)$existing->getIid();
+ if (!isset($this->course->resources['Forum_Category'])) {
+ $this->course->resources['Forum_Category'] = [];
+ }
+ $this->course->resources['Forum_Category'][$id]->destination_id = $destIid;
+ $this->dlog('restore_forum_category: reuse existing', ['title' => $title, 'iid' => $destIid]);
+ continue;
+ }
- if (isset($_SESSION['new_base_foldername']) && isset($_SESSION['new_base_path'])) {
- $new_base_foldername = $_SESSION['new_base_foldername'];
- $new_base_path = $_SESSION['new_base_path'];
- }
+ $cat = (new CForumCategory())
+ ->setTitle($title)
+ ->setCatComment($comment)
+ ->setParent($course)
+ ->addCourseLink($course, $session);
- $dest_document_path = $new_base_path.'/'.$document_path[2]; // e.g: "/var/www/wiener/courses/CURSO4/document/carpeta1_1/subcarpeta1/collaborative.png"
- $basedir_dest_path = dirname($dest_document_path); // e.g: "/var/www/wiener/courses/CURSO4/document/carpeta1_1/subcarpeta1"
- $base_path_document = $course_path.$document_path[0]; // e.g: "/var/www/wiener/courses/CURSO4/document"
- $path_title = '/'.$new_base_foldername.'/'.$document_path[2];
-
- copy_folder_course_session(
- $basedir_dest_path,
- $base_path_document,
- $session_id,
- $course_info,
- $document,
- $this->course_origin_id,
- $originalFolderNameList,
- $originalPath
- );
+ $catRepo->create($cat);
+ $em->flush();
- if (file_exists($course_path.$document->path)) {
- copy($course_path.$document->path, $dest_document_path);
- }
+ $this->course->resources['Forum_Category'][$id]->destination_id = (int)$cat->getIid();
+ $this->dlog('restore_forum_category: created', ['title' => $title, 'iid' => (int)$cat->getIid()]);
+ }
- // Replace old course code with the new destination code see BT#1985
- if (file_exists($dest_document_path)) {
- $file_info = pathinfo($dest_document_path);
- if (in_array($file_info['extension'], ['html', 'htm'])) {
- $content = file_get_contents($dest_document_path);
- if (UTF8_CONVERT) {
- $content = utf8_encode($content);
- }
- $content = DocumentManager::replaceUrlWithNewCourseCode(
- $content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- file_put_contents($dest_document_path, $content);
- $this->fixEditorHtmlContent($dest_document_path, $webEditorCss);
- }
- }
+ $this->dlog('restore_forum_category: done', ['count' => count($bag)]);
+ }
- $title = basename($path_title);
- if (isset($originalFolderNameList[basename($path_title)])) {
- $title = $originalFolderNameList[basename($path_title)];
- }
+ public function restore_forums(int $sessionId = 0): void
+ {
+ $forumsBag = $this->course->resources['forum'] ?? [];
+ if (empty($forumsBag)) {
+ $this->dlog('restore_forums: empty forums bag');
+ return;
+ }
- $params = [
- 'path' => self::DBUTF8($path_title),
- 'c_id' => $this->destination_course_id,
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($title),
- 'filetype' => self::DBUTF8($document->file_type),
- 'size' => self::DBUTF8($document->size),
- 'session_id' => $my_session_id,
- ];
-
- $document_id = Database::insert($table, $params);
-
- if ($document_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $document_id";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $document_id;
-
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
-
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
- $groupInfo = $this->checkGroupId($toGroupId);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $document_id,
- 'DocumentAdded',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- }
- } else {
- if (file_exists($path.$document->path)) {
- copy($path.$document->path, $path.$new_file_name);
- }
- // Replace old course code with the new destination code see BT#1985
- if (file_exists($path.$new_file_name)) {
- $file_info = pathinfo($path.$new_file_name);
- if (in_array($file_info['extension'], ['html', 'htm'])) {
- $content = file_get_contents($path.$new_file_name);
- if (UTF8_CONVERT) {
- $content = utf8_encode($content);
- }
- $content = DocumentManager::replaceUrlWithNewCourseCode(
- $content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- file_put_contents($path.$new_file_name, $content);
- $this->fixEditorHtmlContent($path.$new_file_name, $webEditorCss);
- }
- }
+ $em = Database::getManager();
+ $catRepo = Container::getForumCategoryRepository();
+ $forumRepo = Container::getForumRepository();
- $params = [
- 'path' => '/'.self::DBUTF8escapestring(substr($new_file_name, 9)),
- 'c_id' => $this->destination_course_id,
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($document->title),
- 'filetype' => self::DBUTF8($document->file_type),
- 'size' => self::DBUTF8($document->size),
- 'session_id' => $my_session_id,
- ];
-
- $document_id = Database::insert($table, $params);
-
- if ($document_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $document_id";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $document_id;
-
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
-
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
- $groupInfo = $this->checkGroupId($toGroupId);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $document_id,
- 'DocumentAdded',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- }
- }
- } else {
- copy(
- $this->course->backup_path.'/'.$document->path,
- $path.$new_file_name
- );
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity($sessionId);
- // Replace old course code with the new destination code see BT#1985
- if (file_exists($path.$new_file_name)) {
- $file_info = pathinfo($path.$new_file_name);
- if (in_array($file_info['extension'], ['html', 'htm'])) {
- $content = file_get_contents($path.$new_file_name);
- if (UTF8_CONVERT) {
- $content = utf8_encode($content);
- }
- $content = DocumentManager::replaceUrlWithNewCourseCode(
- $content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- file_put_contents($path.$new_file_name, $content);
- $this->fixEditorHtmlContent($path.$new_file_name, $webEditorCss);
- }
- }
+ // Build/ensure categories
+ $catBag = $this->course->resources['Forum_Category'] ?? $this->course->resources['forum_category'] ?? [];
+ $catMap = [];
- $params = [
- 'c_id' => $this->destination_course_id,
- 'path' => '/'.self::DBUTF8escapestring(substr($new_file_name, 9)),
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($document->title),
- 'filetype' => self::DBUTF8($document->file_type),
- 'size' => self::DBUTF8($document->size),
- 'session_id' => $my_session_id,
- ];
-
- $document_id = Database::insert($table, $params);
-
- if ($document_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $document_id";
- Database::query($sql);
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $document_id;
-
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
-
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
- $groupInfo = $this->checkGroupId($toGroupId);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $document_id,
- 'DocumentAdded',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- }
- }
+ if (!empty($catBag)) {
+ foreach ($catBag as $srcCatId => $res) {
+ if (!empty($res->destination_id)) {
+ $catMap[(int)$srcCatId] = (int)$res->destination_id;
+ continue;
+ }
- break;
- } // end switch
- } else {
- // end if file exists
- //make sure the source file actually exists
- if (is_file($this->course->backup_path.'/'.$document->path) &&
- is_readable($this->course->backup_path.'/'.$document->path) &&
- is_dir(dirname($path.$document->path)) &&
- is_writable(dirname($path.$document->path))
- ) {
- copy(
- $this->course->backup_path.'/'.$document->path,
- $path.$document->path
- );
+ $obj = is_object($res->obj ?? null) ? $res->obj : (object)[];
+ $title = (string)($obj->cat_title ?? $obj->title ?? "Forum category #$srcCatId");
+ $comment = (string)($obj->cat_comment ?? $obj->description ?? '');
- // Replace old course code with the new destination code see BT#1985
- if (file_exists($path.$document->path)) {
- $file_info = pathinfo($path.$document->path);
- if (isset($file_info['extension']) && in_array($file_info['extension'], ['html', 'htm'])) {
- $content = file_get_contents($path.$document->path);
- if (UTF8_CONVERT) {
- $content = utf8_encode($content);
- }
- $content = DocumentManager::replaceUrlWithNewCourseCode(
- $content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- file_put_contents($path.$document->path, $content);
- $this->fixEditorHtmlContent($path.$document->path, $webEditorCss);
- }
- }
+ $cat = (new CForumCategory())
+ ->setTitle($title)
+ ->setCatComment($comment)
+ ->setParent($course)
+ ->addCourseLink($course, $session);
- $params = [
- 'c_id' => $this->destination_course_id,
- 'path' => '/'.self::DBUTF8(substr($document->path, 9)),
- 'comment' => self::DBUTF8($document->comment),
- 'title' => self::DBUTF8($document->title),
- 'filetype' => self::DBUTF8($document->file_type),
- 'size' => self::DBUTF8($document->size),
- 'session_id' => $my_session_id,
- 'readonly' => 0,
- ];
+ $catRepo->create($cat);
+ $em->flush();
- $document_id = Database::insert($table, $params);
-
- if ($document_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $document_id";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_DOCUMENT][$id]->destination_id = $document_id;
-
- $itemProperty = isset($document->item_properties[0]) ? $document->item_properties[0] : '';
- $insertUserId = isset($itemProperty['insert_user_id']) ? $itemProperty['insert_user_id'] : api_get_user_id();
- $toGroupId = isset($itemProperty['to_group_id']) ? $itemProperty['to_group_id'] : 0;
- $toUserId = isset($itemProperty['to_user_id']) ? $itemProperty['to_user_id'] : null;
-
- $insertUserId = $this->checkUserId($insertUserId);
- $toUserId = $this->checkUserId($toUserId, true);
- $groupInfo = $this->checkGroupId($toGroupId);
-
- /*api_item_property_update(
- $course_info,
- TOOL_DOCUMENT,
- $document_id,
- 'DocumentAdded',
- $insertUserId,
- $groupInfo,
- $toUserId,
- null,
- null,
- $my_session_id
- );*/
- }
- } else {
- // There was an error in checking existence and
- // permissions for files to copy. Try to determine
- // the exact issue
- // Issue with origin document?
- if (!is_file($this->course->backup_path.'/'.$document->path)) {
- error_log(
- 'Course copy generated an ignorable error while trying to copy '.
- $this->course->backup_path.'/'.$document->path.': origin file not found'
- );
- } elseif (!is_readable($this->course->backup_path.'/'.$document->path)) {
- error_log(
- 'Course copy generated an ignorable error while trying to copy '.
- $this->course->backup_path.'/'.$document->path.': origin file not readable'
- );
- }
- // Issue with destination directories?
- if (!is_dir(dirname($path.$document->path))) {
- error_log(
- 'Course copy generated an ignorable error while trying to copy '.
- $this->course->backup_path.'/'.$document->path.' to '.
- dirname($path.$document->path).': destination directory not found'
- );
- }
- if (!is_writable(dirname($path.$document->path))) {
- error_log(
- 'Course copy generated an ignorable error while trying to copy '.
- $this->course->backup_path.'/'.$document->path.' to '.
- dirname($path.$document->path).': destination directory not writable'
- );
- }
- }
- } // end file doesn't exist
- }
-
- // add image information for area questions
- if (preg_match('/^quiz-.*$/', $document->title) &&
- preg_match('/^document\/images\/.*$/', $document->path)
- ) {
- $this->course->resources[RESOURCE_DOCUMENT]['image_quiz'][$document->title] = [
- 'path' => $document->path,
- 'title' => $document->title,
- 'source_id' => $document->source_id,
- 'destination_id' => $document->destination_id,
- ];
+ $destIid = (int)$cat->getIid();
+ $catMap[(int)$srcCatId] = $destIid;
+
+ if (!isset($this->course->resources['Forum_Category'])) {
+ $this->course->resources['Forum_Category'] = [];
+ }
+ $this->course->resources['Forum_Category'][$srcCatId]->destination_id = $destIid;
+
+ $this->dlog('restore_forums: created category', ['src_id' => (int)$srcCatId, 'iid' => $destIid, 'title' => $title]);
}
- } // end for each
+ }
- // Delete sessions for the copy the new folder in session
- unset($_SESSION['new_base_foldername']);
- unset($_SESSION['orig_base_foldername']);
- unset($_SESSION['new_base_path']);
- }
+ // Default category "General" if needed
+ $defaultCategory = null;
+ $ensureDefault = function() use (&$defaultCategory, $course, $session, $catRepo, $em): CForumCategory {
+ if ($defaultCategory instanceof CForumCategory) {
+ return $defaultCategory;
+ }
+ $defaultCategory = (new CForumCategory())
+ ->setTitle('General')
+ ->setCatComment('')
+ ->setParent($course)
+ ->addCourseLink($course, $session);
+ $catRepo->create($defaultCategory);
+ $em->flush();
+ return $defaultCategory;
+ };
+
+ // Create forums and their topics
+ foreach ($forumsBag as $srcForumId => $forumRes) {
+ if (!is_object($forumRes) || !is_object($forumRes->obj)) { continue; }
+ $p = (array)$forumRes->obj;
+
+ $dstCategory = null;
+ $srcCatId = (int)($p['forum_category'] ?? 0);
+ if ($srcCatId > 0 && isset($catMap[$srcCatId])) {
+ $dstCategory = $catRepo->find($catMap[$srcCatId]);
+ }
+ if (!$dstCategory && count($catMap) === 1) {
+ $onlyDestIid = (int)reset($catMap);
+ $dstCategory = $catRepo->find($onlyDestIid);
+ }
+ if (!$dstCategory) {
+ $dstCategory = $ensureDefault();
+ }
- /**
- * Restore scorm documents
- * TODO @TODO check that the restore function with renaming doesn't break the scorm structure!
- * see #7029.
- */
- public function restore_scorm_documents()
+ $forum = (new CForum())
+ ->setTitle($p['forum_title'] ?? ('Forum #'.$srcForumId))
+ ->setForumComment((string)($p['forum_comment'] ?? ''))
+ ->setForumCategory($dstCategory)
+ ->setAllowAnonymous((int)($p['allow_anonymous'] ?? 0))
+ ->setAllowEdit((int)($p['allow_edit'] ?? 0))
+ ->setApprovalDirectPost((string)($p['approval_direct_post'] ?? '0'))
+ ->setAllowAttachments((int)($p['allow_attachments'] ?? 1))
+ ->setAllowNewThreads((int)($p['allow_new_threads'] ?? 1))
+ ->setDefaultView($p['default_view'] ?? 'flat')
+ ->setForumOfGroup((string)($p['forum_of_group'] ?? 0))
+ ->setForumGroupPublicPrivate($p['forum_group_public_private'] ?? 'public')
+ ->setModerated((bool)($p['moderated'] ?? false))
+ ->setStartTime(!empty($p['start_time']) && $p['start_time'] !== '0000-00-00 00:00:00'
+ ? api_get_utc_datetime($p['start_time'], true, true) : null)
+ ->setEndTime(!empty($p['end_time']) && $p['end_time'] !== '0000-00-00 00:00:00'
+ ? api_get_utc_datetime($p['end_time'], true, true) : null)
+ ->setParent($dstCategory ?: $course)
+ ->addCourseLink($course, $session);
+
+ $forumRepo->create($forum);
+ $em->flush();
+
+ $this->course->resources['forum'][$srcForumId]->destination_id = (int)$forum->getIid();
+ $this->dlog('restore_forums: created forum', [
+ 'src_forum_id' => (int)$srcForumId,
+ 'dst_forum_iid'=> (int)$forum->getIid(),
+ 'category_iid' => (int)$dstCategory->getIid(),
+ ]);
+
+ // Topics of this forum
+ $topicsBag = $this->course->resources['thread'] ?? [];
+ foreach ($topicsBag as $srcThreadId => $topicRes) {
+ if (!is_object($topicRes) || !is_object($topicRes->obj)) { continue; }
+ if ((int)$topicRes->obj->forum_id === (int)$srcForumId) {
+ $tid = $this->restore_topic((int)$srcThreadId, (int)$forum->getIid(), $sessionId);
+ $this->dlog('restore_forums: topic restored', [
+ 'src_thread_id' => (int)$srcThreadId,
+ 'dst_thread_iid'=> (int)($tid ?? 0),
+ 'dst_forum_iid' => (int)$forum->getIid(),
+ ]);
+ }
+ }
+ }
+
+ $this->dlog('restore_forums: done', ['forums' => count($forumsBag)]);
+ }
+
+ public function restore_topic(int $srcThreadId, int $dstForumId, int $sessionId = 0): ?int
{
- /*$perm = api_get_permissions_for_new_directories();
- if ($this->course->has_resources(RESOURCE_SCORM)) {
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_SCORM] as $document) {
- $path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/';
- @mkdir(dirname($path.$document->path), $perm, true);
- if (file_exists($path.$document->path)) {
- switch ($this->file_option) {
- case FILE_OVERWRITE:
- rmdirr($path.$document->path);
- copyDirTo(
- $this->course->backup_path.'/'.$document->path,
- $path.$document->path,
- false
- );
+ $topicsBag = $this->course->resources['thread'] ?? [];
+ $topicRes = $topicsBag[$srcThreadId] ?? null;
+ if (!$topicRes || !is_object($topicRes->obj)) {
+ $this->dlog('restore_topic: missing topic object', ['src_thread_id' => $srcThreadId]);
+ return null;
+ }
- break;
- case FILE_SKIP:
- break;
- case FILE_RENAME:
- $i = 1;
- $ext = explode('.', basename($document->path));
- if (count($ext) > 1) {
- $ext = array_pop($ext);
- $file_name_no_ext = substr($document->path, 0, -(strlen($ext) + 1));
- $ext = '.'.$ext;
- } else {
- $ext = '';
- $file_name_no_ext = $document->path;
- }
+ $em = Database::getManager();
+ $forumRepo = Container::getForumRepository();
+ $threadRepo = Container::getForumThreadRepository();
+ $postRepo = Container::getForumPostRepository();
- $new_file_name = $file_name_no_ext.'_'.$i.$ext;
- $file_exists = file_exists($path.$new_file_name);
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity((int)$sessionId);
+ $user = api_get_user_entity($this->first_teacher_id);
- while ($file_exists) {
- $i++;
- $new_file_name = $file_name_no_ext.'_'.$i.$ext;
- $file_exists = file_exists($path.$new_file_name);
- }
+ /** @var CForum|null $forum */
+ $forum = $forumRepo->find($dstForumId);
+ if (!$forum) {
+ $this->dlog('restore_topic: destination forum not found', ['dst_forum_id' => $dstForumId]);
+ return null;
+ }
- rename(
- $this->course->backup_path.'/'.$document->path,
- $this->course->backup_path.'/'.$new_file_name
- );
- copyDirTo(
- $this->course->backup_path.'/'.$new_file_name,
- $path.dirname($new_file_name),
- false
- );
- rename(
- $this->course->backup_path.'/'.$new_file_name,
- $this->course->backup_path.'/'.$document->path
- );
+ $p = (array)$topicRes->obj;
+
+ $thread = (new CForumThread())
+ ->setTitle((string)($p['thread_title'] ?? "Thread #$srcThreadId"))
+ ->setForum($forum)
+ ->setUser($user)
+ ->setThreadDate(new \DateTime(api_get_utc_datetime(), new \DateTimeZone('UTC')))
+ ->setThreadSticky((bool)($p['thread_sticky'] ?? false))
+ ->setThreadTitleQualify((string)($p['thread_title_qualify'] ?? ''))
+ ->setThreadQualifyMax((float)($p['thread_qualify_max'] ?? 0))
+ ->setThreadWeight((float)($p['thread_weight'] ?? 0))
+ ->setThreadPeerQualify((bool)($p['thread_peer_qualify'] ?? false))
+ ->setParent($forum)
+ ->addCourseLink($course, $session);
+
+ $threadRepo->create($thread);
+ $em->flush();
+
+ $this->course->resources['thread'][$srcThreadId]->destination_id = (int)$thread->getIid();
+ $this->dlog('restore_topic: created', [
+ 'src_thread_id' => $srcThreadId,
+ 'dst_thread_iid'=> (int)$thread->getIid(),
+ 'dst_forum_iid' => (int)$forum->getIid(),
+ ]);
+
+ // Posts
+ $postsBag = $this->course->resources[ 'post'] ?? [];
+ foreach ($postsBag as $srcPostId => $postRes) {
+ if (!is_object($postRes) || !is_object($postRes->obj)) { continue; }
+ if ((int)$postRes->obj->thread_id === (int)$srcThreadId) {
+ $pid = $this->restore_post((int)$srcPostId, (int)$thread->getIid(), (int)$forum->getIid(), $sessionId);
+ $this->dlog('restore_topic: post restored', ['src_post_id' => (int)$srcPostId, 'dst_post_iid' => (int)($pid ?? 0)]);
+ }
+ }
- break;
- } // end switch
- } else {
- // end if file exists
- copyDirTo(
- $this->course->backup_path.'/'.$document->path,
- $path.$document->path,
- false
- );
- }
- } // end for each
- }*/
+ $last = $postRepo->findOneBy(['thread' => $thread], ['postDate' => 'DESC']);
+ if ($last) {
+ $thread->setThreadLastPost($last);
+ $em->persist($thread);
+ $em->flush();
+ }
+
+ return (int)$thread->getIid();
}
- /**
- * Restore forums.
- *
- * @param int $sessionId
- */
- public function restore_forums($sessionId = 0)
+ public function restore_post(int $srcPostId, int $dstThreadId, int $dstForumId, int $sessionId = 0): ?int
{
- if ($this->course->has_resources(RESOURCE_FORUM)) {
- $sessionId = (int) $sessionId;
- $table_forum = Database::get_course_table(TABLE_FORUM);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_FORUM] as $id => $forum) {
- $params = (array) $forum->obj;
- $cat_id = '';
- if (isset($this->course->resources[RESOURCE_FORUMCATEGORY]) &&
- isset($this->course->resources[RESOURCE_FORUMCATEGORY][$params['forum_category']])) {
- if (-1 == $this->course->resources[RESOURCE_FORUMCATEGORY][$params['forum_category']]->destination_id) {
- $cat_id = $this->restore_forum_category($params['forum_category'], $sessionId);
- } else {
- $cat_id = $this->course->resources[RESOURCE_FORUMCATEGORY][$params['forum_category']]->destination_id;
- }
+ $postsBag = $this->course->resources['post'] ?? [];
+ $postRes = $postsBag[$srcPostId] ?? null;
+ if (!$postRes || !is_object($postRes->obj)) {
+ $this->dlog('restore_post: missing post object', ['src_post_id' => $srcPostId]);
+ return null;
+ }
+
+ $em = Database::getManager();
+ $forumRepo = Container::getForumRepository();
+ $threadRepo = Container::getForumThreadRepository();
+ $postRepo = Container::getForumPostRepository();
+
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity((int)$sessionId);
+ $user = api_get_user_entity($this->first_teacher_id);
+
+ $thread = $threadRepo->find($dstThreadId);
+ $forum = $forumRepo->find($dstForumId);
+ if (!$thread || !$forum) {
+ $this->dlog('restore_post: destination thread/forum not found', [
+ 'dst_thread_id' => $dstThreadId,
+ 'dst_forum_id' => $dstForumId,
+ ]);
+ return null;
+ }
+
+ $p = (array)$postRes->obj;
+
+ $post = (new CForumPost())
+ ->setTitle((string)($p['post_title'] ?? "Post #$srcPostId"))
+ ->setPostText((string)($p['post_text'] ?? ''))
+ ->setThread($thread)
+ ->setForum($forum)
+ ->setUser($user)
+ ->setPostDate(new \DateTime(api_get_utc_datetime(), new \DateTimeZone('UTC')))
+ ->setPostNotification((bool)($p['post_notification'] ?? false))
+ ->setVisible(true)
+ ->setStatus(CForumPost::STATUS_VALIDATED)
+ ->setParent($thread)
+ ->addCourseLink($course, $session);
+
+ if (!empty($p['post_parent_id'])) {
+ $parentDestId = (int)($postsBag[$p['post_parent_id']]->destination_id ?? 0);
+ if ($parentDestId > 0) {
+ $parent = $postRepo->find($parentDestId);
+ if ($parent) {
+ $post->setPostParent($parent);
}
+ }
+ }
- $params = self::DBUTF8_array($params);
- $params['c_id'] = $this->destination_course_id;
- $params['forum_category'] = $cat_id;
- $params['session_id'] = $sessionId;
- $params['start_time'] = isset($params['start_time']) && '0000-00-00 00:00:00' === $params['start_time'] ? null : $params['start_time'];
- $params['end_time'] = isset($params['end_time']) && '0000-00-00 00:00:00' === $params['end_time'] ? null : $params['end_time'];
- $params['forum_id'] = 0;
- unset($params['iid']);
+ $postRepo->create($post);
+ $em->flush();
- $params['forum_comment'] = DocumentManager::replaceUrlWithNewCourseCode(
- $params['forum_comment'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ $this->course->resources['post'][$srcPostId]->destination_id = (int)$post->getIid();
+ $this->dlog('restore_post: created', [
+ 'src_post_id' => (int)$srcPostId,
+ 'dst_post_iid' => (int)$post->getIid(),
+ 'dst_thread_id' => (int)$thread->getIid(),
+ 'dst_forum_id' => (int)$forum->getIid(),
+ ]);
- if (!empty($params['forum_image'])) {
- $original_forum_image = $this->course->path.'upload/forum/images/'.$params['forum_image'];
- if (file_exists($original_forum_image)) {
- $new_forum_image = api_get_path(SYS_COURSE_PATH).
- $this->destination_course_info['path'].'/upload/forum/images/'.$params['forum_image'];
- @copy($original_forum_image, $new_forum_image);
- }
- }
+ return (int)$post->getIid();
+ }
- $new_id = Database::insert($table_forum, $params);
+ public function restore_link_category($id, $sessionId = 0)
+ {
+ $sessionId = (int) $sessionId;
- if ($new_id) {
- $sql = "UPDATE $table_forum SET forum_id = iid WHERE iid = $new_id";
- Database::query($sql);
+ // "No category" short-circuit (legacy used 0 as 'uncategorized').
+ if (0 === (int) $id) {
+ $this->dlog('restore_link_category: source category is 0 (no category), returning 0');
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_FORUM,
- $new_id,
- 'ForumUpdated',
- api_get_user_id()
- );*/
+ return 0;
+ }
- $this->course->resources[RESOURCE_FORUM][$id]->destination_id = $new_id;
+ $resources = $this->course->resources ?? [];
+ $srcCat = $resources[RESOURCE_LINKCATEGORY][$id] ?? null;
- $forum_topics = 0;
- if (isset($this->course->resources[RESOURCE_FORUMTOPIC]) &&
- is_array($this->course->resources[RESOURCE_FORUMTOPIC])
- ) {
- foreach ($this->course->resources[RESOURCE_FORUMTOPIC] as $topic_id => $topic) {
- if ($topic->obj->forum_id == $id) {
- $this->restore_topic($topic_id, $new_id, $sessionId);
- $forum_topics++;
- }
- }
+ if (!is_object($srcCat)) {
+ error_log('COURSE_DEBUG: restore_link_category: source category object not found for id ' . $id);
+
+ return 0;
+ }
+
+ // Already restored?
+ if (!empty($srcCat->destination_id)) {
+ return (int) $srcCat->destination_id;
+ }
+
+ $em = Database::getManager();
+ $catRepo = Container::getLinkCategoryRepository();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity($sessionId);
+
+ // Normalize incoming values
+ $title = (string) ($srcCat->title ?? $srcCat->category_title ?? 'Links');
+ $description = (string) ($srcCat->description ?? '');
+
+ // Try to find existing category by *title* under this course (we'll filter by course parent in PHP)
+ $candidates = $catRepo->findBy(['title' => $title]);
+
+ $existing = null;
+ if (!empty($candidates)) {
+ $courseNode = $course->getResourceNode();
+ foreach ($candidates as $cand) {
+ $node = method_exists($cand, 'getResourceNode') ? $cand->getResourceNode() : null;
+ $parent = $node && method_exists($node, 'getParent') ? $node->getParent() : null;
+ if ($parent && $courseNode && $parent->getId() === $courseNode->getId()) {
+ $existing = $cand;
+ break;
+ }
+ }
+ }
+
+ // Collision handling
+ if ($existing) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $destIid = (int) $existing->getIid();
+ $this->course->resources[RESOURCE_LINKCATEGORY][$id]->destination_id = $destIid;
+ $this->dlog('restore_link_category: reuse (SKIP)', [
+ 'src_cat_id' => (int) $id,
+ 'dst_cat_id' => $destIid,
+ 'title' => $title,
+ ]);
+
+ return $destIid;
+
+ case FILE_OVERWRITE:
+ // Update description (keep title)
+ $existing->setDescription($description);
+ // Ensure course/session link
+ if (method_exists($existing, 'setParent')) {
+ $existing->setParent($course);
}
- if ($forum_topics > 0) {
- $sql = 'UPDATE '.$table_forum.' SET forum_threads = '.$forum_topics."
- WHERE c_id = {$this->destination_course_id} AND forum_id = ".(int) $new_id;
- Database::query($sql);
+ if (method_exists($existing, 'addCourseLink')) {
+ $existing->addCourseLink($course, $session);
}
- }
+
+ $em->persist($existing);
+ $em->flush();
+
+ $destIid = (int) $existing->getIid();
+ $this->course->resources[RESOURCE_LINKCATEGORY][$id]->destination_id = $destIid;
+ $this->dlog('restore_link_category: overwrite', [
+ 'src_cat_id' => (int) $id,
+ 'dst_cat_id' => $destIid,
+ 'title' => $title,
+ ]);
+
+ return $destIid;
+
+ case FILE_RENAME:
+ default:
+ // Create a new unique title inside the same course parent
+ $base = $title;
+ $i = 1;
+ do {
+ $title = $base . ' (' . $i . ')';
+ $candidates = $catRepo->findBy(['title' => $title]);
+ $exists = false;
+
+ if (!empty($candidates)) {
+ $courseNode = $course->getResourceNode();
+ foreach ($candidates as $cand) {
+ $node = method_exists($cand, 'getResourceNode') ? $cand->getResourceNode() : null;
+ $parent = $node && method_exists($node, 'getParent') ? $node->getParent() : null;
+ if ($parent && $courseNode && $parent->getId() === $courseNode->getId()) {
+ $exists = true;
+ break;
+ }
+ }
+ }
+
+ $i++;
+ } while ($exists);
+ break;
}
}
+
+ // Create new category
+ $cat = (new CLinkCategory())
+ ->setTitle($title)
+ ->setDescription($description);
+
+ if (method_exists($cat, 'setParent')) {
+ $cat->setParent($course); // parent ResourceNode: Course
+ }
+ if (method_exists($cat, 'addCourseLink')) {
+ $cat->addCourseLink($course, $session); // visibility link (course, session)
+ }
+
+ $em->persist($cat);
+ $em->flush();
+
+ $destIid = (int) $cat->getIid();
+ $this->course->resources[RESOURCE_LINKCATEGORY][$id]->destination_id = $destIid;
+
+ $this->dlog('restore_link_category: created', [
+ 'src_cat_id' => (int) $id,
+ 'dst_cat_id' => $destIid,
+ 'title' => (string) $title,
+ ]);
+
+ return $destIid;
}
- /**
- * Restore forum-categories.
- */
- public function restore_forum_category($my_id = null, $sessionId = 0)
+ public function restore_links($session_id = 0)
{
- $forum_cat_table = Database::get_course_table(TABLE_FORUM_CATEGORY);
+ if (!$this->course->has_resources(RESOURCE_LINK)) {
+ return;
+ }
+
$resources = $this->course->resources;
- $sessionId = (int) $sessionId;
- if (!empty($resources[RESOURCE_FORUMCATEGORY])) {
- foreach ($resources[RESOURCE_FORUMCATEGORY] as $id => $forum_cat) {
- if (!empty($my_id)) {
- if ($my_id != $id) {
- continue;
- }
+ $count = is_array($resources[RESOURCE_LINK] ?? null) ? count($resources[RESOURCE_LINK]) : 0;
+
+ $this->dlog('restore_links: begin', ['count' => $count]);
+
+ $em = Database::getManager();
+ $linkRepo = Container::getLinkRepository();
+ $catRepo = Container::getLinkCategoryRepository();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity((int) $session_id);
+
+ // Safe duplicate finder (no dot-path in criteria; filter parent in PHP)
+ $findDuplicate = function (string $t, string $u, ?CLinkCategory $cat) use ($linkRepo, $course) {
+ $criteria = ['title' => $t, 'url' => $u];
+ $criteria['category'] = $cat instanceof CLinkCategory ? $cat : null;
+
+ $candidates = $linkRepo->findBy($criteria);
+ if (empty($candidates)) {
+ return null;
+ }
+
+ $courseNode = $course->getResourceNode();
+ foreach ($candidates as $cand) {
+ $node = method_exists($cand, 'getResourceNode') ? $cand->getResourceNode() : null;
+ $parent = $node && method_exists($node, 'getParent') ? $node->getParent() : null;
+ if ($parent && $courseNode && $parent->getId() === $courseNode->getId()) {
+ return $cand;
}
- if ($forum_cat && !$forum_cat->is_restored()) {
- $params = (array) $forum_cat->obj;
- $params['c_id'] = $this->destination_course_id;
- $params['cat_comment'] = DocumentManager::replaceUrlWithNewCourseCode(
- $params['cat_comment'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- $params['session_id'] = $sessionId;
- $params['cat_id'] = 0;
- unset($params['iid']);
-
- $params = self::DBUTF8_array($params);
- $new_id = Database::insert($forum_cat_table, $params);
-
- if ($new_id) {
- $sql = "UPDATE $forum_cat_table SET cat_id = iid WHERE iid = $new_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_FORUM_CATEGORY,
- $new_id,
- 'ForumCategoryUpdated',
- api_get_user_id()
- );*/
- $this->course->resources[RESOURCE_FORUMCATEGORY][$id]->destination_id = $new_id;
+ }
+
+ return null;
+ };
+
+ foreach ($resources[RESOURCE_LINK] as $oldLinkId => $link) {
+ // Normalize (accept values from object or "extra")
+ $rawUrl = (string) ($link->url ?? ($link->extra['url'] ?? ''));
+ $rawTitle = (string) ($link->title ?? ($link->extra['title'] ?? ''));
+ $rawDesc = (string) ($link->description ?? ($link->extra['description'] ?? ''));
+ $target = isset($link->target) ? (string) $link->target : null;
+ $catSrcId = (int) ($link->category_id ?? 0);
+ $onHome = (bool) ($link->on_homepage ?? false);
+
+ $url = trim($rawUrl);
+ $title = trim($rawTitle) !== '' ? trim($rawTitle) : $url;
+
+ if ($url === '') {
+ $this->dlog('restore_links: skipped (empty URL)', [
+ 'src_link_id' => (int) $oldLinkId,
+ 'has_obj' => !empty($link->has_obj),
+ 'extra_keys' => isset($link->extra) ? implode(',', array_keys((array) $link->extra)) : '',
+ ]);
+ continue;
+ }
+
+ // Resolve / create destination category if source had one; otherwise null
+ $category = null;
+ if ($catSrcId > 0) {
+ $dstCatIid = (int) $this->restore_link_category($catSrcId, (int) $session_id);
+ if ($dstCatIid > 0) {
+ $category = $catRepo->find($dstCatIid);
+ } else {
+ $this->dlog('restore_links: category not available, using null', [
+ 'src_link_id' => (int) $oldLinkId,
+ 'src_cat_id' => (int) $catSrcId,
+ ]);
+ }
+ }
+
+ // Duplicate handling (title + url + category in same course)
+ $existing = $findDuplicate($title, $url, $category);
+
+ if ($existing) {
+ if ($this->file_option === FILE_SKIP) {
+ $destIid = (int) $existing->getIid();
+ $this->course->resources[RESOURCE_LINK][$oldLinkId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_LINK][$oldLinkId]->destination_id = $destIid;
+
+ $this->dlog('restore_links: reuse (SKIP)', [
+ 'src_link_id' => (int) $oldLinkId,
+ 'dst_link_id' => $destIid,
+ 'title' => $title,
+ 'url' => $url,
+ ]);
+
+ continue;
+ }
+
+ if ($this->file_option === FILE_OVERWRITE) {
+ // Update main fields (keep position/shortcut logic outside)
+ $existing
+ ->setUrl($url)
+ ->setTitle($title)
+ ->setDescription($rawDesc) // rewrite to assets after flush
+ ->setTarget((string) ($target ?? ''));
+
+ if (method_exists($existing, 'setParent')) {
+ $existing->setParent($course);
}
+ if (method_exists($existing, 'addCourseLink')) {
+ $existing->addCourseLink($course, $session);
+ }
+ $existing->setCategory($category); // can be null
+
+ $em->persist($existing);
+ $em->flush();
- if (!empty($my_id)) {
- return $new_id;
+ // Now rewrite legacy "document/..." URLs inside description to Assets
+ try {
+ $backupRoot = $this->course->backup_path ?? '';
+ $extraRoots = array_filter([
+ $this->course->destination_path ?? '',
+ $this->course->origin_path ?? '',
+ ]);
+ $rewritten = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $rawDesc,
+ $existing,
+ $backupRoot,
+ $extraRoots
+ );
+
+ if ($rewritten !== $rawDesc) {
+ $existing->setDescription($rewritten);
+ $em->persist($existing);
+ $em->flush();
+ }
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_links: asset rewrite failed (overwrite): ' . $e->getMessage());
}
+
+ $destIid = (int) $existing->getIid();
+ $this->course->resources[RESOURCE_LINK][$oldLinkId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_LINK][$oldLinkId]->destination_id = $destIid;
+
+ $this->dlog('restore_links: overwrite', [
+ 'src_link_id' => (int) $oldLinkId,
+ 'dst_link_id' => $destIid,
+ 'title' => $title,
+ 'url' => $url,
+ ]);
+
+ continue;
}
+
+ // FILE_RENAME (default): make title unique among same course/category
+ $base = $title;
+ $i = 1;
+ do {
+ $title = $base . ' (' . $i . ')';
+ $i++;
+ } while ($findDuplicate($title, $url, $category));
}
- }
- }
- /**
- * Restore a forum-topic.
- *
- * @param false|string $forum_id
- *
- * @return int
- */
- public function restore_topic($thread_id, $forum_id, $sessionId = 0)
- {
- $table = Database::get_course_table(TABLE_FORUM_THREAD);
- $topic = $this->course->resources[RESOURCE_FORUMTOPIC][$thread_id];
+ // Create new link entity
+ $entity = (new CLink())
+ ->setUrl($url)
+ ->setTitle($title)
+ ->setDescription($rawDesc) // rewrite to assets after first flush
+ ->setTarget((string) ($target ?? ''));
- $sessionId = (int) $sessionId;
- $params = (array) $topic->obj;
- $params = self::DBUTF8_array($params);
- $params['c_id'] = $this->destination_course_id;
- $params['forum_id'] = $forum_id;
- $params['thread_poster_id'] = $this->first_teacher_id;
- $params['thread_date'] = api_get_utc_datetime();
- $params['thread_close_date'] = null;
- $params['thread_last_post'] = 0;
- $params['thread_replies'] = 0;
- $params['thread_views'] = 0;
- $params['session_id'] = $sessionId;
- $params['thread_id'] = 0;
-
- unset($params['iid']);
-
- $new_id = Database::insert($table, $params);
-
- if ($new_id) {
- $sql = "UPDATE $table SET thread_id = iid WHERE iid = $new_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_FORUM_THREAD,
- $new_id,
- 'ThreadAdded',
- api_get_user_id(),
- 0,
- 0,
- null,
- null,
- $sessionId
- );*/
-
- $this->course->resources[RESOURCE_FORUMTOPIC][$thread_id]->destination_id = $new_id;
- foreach ($this->course->resources[RESOURCE_FORUMPOST] as $post_id => $post) {
- if ($post->obj->thread_id == $thread_id) {
- $this->restore_post($post_id, $new_id, $forum_id, $sessionId);
+ if (method_exists($entity, 'setParent')) {
+ $entity->setParent($course); // parent ResourceNode: Course
+ }
+ if (method_exists($entity, 'addCourseLink')) {
+ $entity->addCourseLink($course, $session); // visibility (course, session)
+ }
+
+ if ($category instanceof CLinkCategory) {
+ $entity->setCategory($category);
+ }
+
+ // Persist to create the ResourceNode; we need it for Asset attachment
+ $em->persist($entity);
+ $em->flush();
+
+ // Rewrite legacy "document/..." URLs inside description to Assets, then save if changed
+ try {
+ $backupRoot = $this->course->backup_path ?? '';
+ $extraRoots = array_filter([
+ $this->course->destination_path ?? '',
+ $this->course->origin_path ?? '',
+ ]);
+ $rewritten = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $rawDesc,
+ $entity,
+ (string) $backupRoot,
+ $extraRoots
+ );
+
+ if ($rewritten !== (string) $rawDesc) {
+ $entity->setDescription($rewritten);
+ $em->persist($entity);
+ $em->flush();
+ }
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_links: asset rewrite failed (create): ' . $e->getMessage());
+ }
+
+ // Map destination id back into resources
+ $destIid = (int) $entity->getIid();
+
+ if (!isset($this->course->resources[RESOURCE_LINK][$oldLinkId])) {
+ $this->course->resources[RESOURCE_LINK][$oldLinkId] = new \stdClass();
+ }
+ $this->course->resources[RESOURCE_LINK][$oldLinkId]->destination_id = $destIid;
+
+ $this->dlog('restore_links: created', [
+ 'src_link_id' => (int) $oldLinkId,
+ 'dst_link_id' => $destIid,
+ 'title' => $title,
+ 'url' => $url,
+ 'category' => $category ? $category->getTitle() : null,
+ ]);
+
+ // Optional: emulate "show on homepage" by ensuring ResourceLink exists (UI/Controller handles real shortcut)
+ if (!empty($onHome)) {
+ try {
+ // Ensure resource link is persisted (it already is via addCourseLink)
+ // Any actual shortcut creation should be delegated to the appropriate service/controller.
+ $em->persist($entity);
+ $em->flush();
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_links: homepage flag handling failed: ' . $e->getMessage());
}
}
}
- return $new_id;
+ $this->dlog('restore_links: end');
}
- /**
- * Restore a forum-post.
- *
- * @TODO Restore tree-structure of posts. For example: attachments to posts.
- *
- * @param false|string $topic_id
- *
- * @return int
- */
- public function restore_post($id, $topic_id, $forum_id, $sessionId = 0)
+ public function restore_tool_intro($sessionId = 0)
{
- $table_post = Database::get_course_table(TABLE_FORUM_POST);
- $post = $this->course->resources[RESOURCE_FORUMPOST][$id];
- $params = (array) $post->obj;
- $params['c_id'] = $this->destination_course_id;
- $params['forum_id'] = $forum_id;
- $params['thread_id'] = $topic_id;
- $params['poster_id'] = $this->first_teacher_id;
- $params['post_date'] = api_get_utc_datetime();
- $params['post_id'] = 0;
- unset($params['iid']);
-
- $params['post_text'] = DocumentManager::replaceUrlWithNewCourseCode(
- $params['post_text'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- $new_id = Database::insert($table_post, $params);
-
- if ($new_id) {
- $sql = "UPDATE $table_post SET post_id = iid WHERE iid = $new_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_FORUM_POST,
- $new_id,
- 'PostAdded',
- api_get_user_id(),
- 0,
- 0,
- null,
- null,
- $sessionId
- );*/
- $this->course->resources[RESOURCE_FORUMPOST][$id]->destination_id = $new_id;
+ $resources = $this->course->resources ?? [];
+ $bagKey = null;
+ if ($this->course->has_resources(RESOURCE_TOOL_INTRO)) {
+ $bagKey = RESOURCE_TOOL_INTRO;
+ } elseif (!empty($resources['Tool introduction'])) {
+ $bagKey = 'Tool introduction';
+ }
+ if ($bagKey === null || empty($resources[$bagKey]) || !is_array($resources[$bagKey])) {
+ return;
}
- return $new_id;
- }
+ $sessionId = (int) $sessionId;
+ $this->dlog('restore_tool_intro: begin', ['count' => count($resources[$bagKey])]);
- /**
- * Restore links.
- */
- public function restore_links($session_id = 0)
- {
- if ($this->course->has_resources(RESOURCE_LINK)) {
- $link_table = Database::get_course_table(TABLE_LINK);
- $resources = $this->course->resources;
+ $em = \Database::getManager();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = $sessionId ? api_get_session_entity($sessionId) : null;
- foreach ($resources[RESOURCE_LINK] as $oldLinkId => $link) {
- $cat_id = (int) $this->restore_link_category($link->category_id, $session_id);
- $sql = "SELECT MAX(display_order)
- FROM $link_table
- WHERE
- c_id = ".$this->destination_course_id." AND
- category_id='".$cat_id."'";
- $result = Database::query($sql);
- list($max_order) = Database::fetch_array($result);
-
- $params = [];
- if (!empty($session_id)) {
- $params['session_id'] = $session_id;
- }
-
- $params['c_id'] = $this->destination_course_id;
- $params['url'] = self::DBUTF8($link->url);
- $params['title'] = self::DBUTF8($link->title);
- $params['description'] = self::DBUTF8($link->description);
- $params['category_id'] = $cat_id;
- $params['on_homepage'] = $link->on_homepage;
- $params['display_order'] = $max_order + 1;
- $params['target'] = $link->target;
-
- $id = Database::insert($link_table, $params);
-
- if ($id) {
- $sql = "UPDATE $link_table SET id = iid WHERE iid = $id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_LINK,
- $id,
- 'LinkAdded',
- api_get_user_id()
- );*/
-
- if (!isset($this->course->resources[RESOURCE_LINK][$oldLinkId])) {
- $this->course->resources[RESOURCE_LINK][$oldLinkId] = new stdClass();
- }
- $this->course->resources[RESOURCE_LINK][$oldLinkId]->destination_id = $id;
+ $toolRepo = $em->getRepository(Tool::class);
+ $cToolRepo = $em->getRepository(CTool::class);
+ $introRepo = $em->getRepository(CToolIntro::class);
+
+ $rewriteContent = function (string $html) {
+ if ($html === '') return '';
+ try {
+ if (class_exists(ChamiloHelper::class)
+ && method_exists(ChamiloHelper::class, 'rewriteLegacyCourseUrlsToAssets')
+ ) {
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $html,
+ api_get_course_entity($this->destination_course_id),
+ (string)($this->course->backup_path ?? ''),
+ array_filter([
+ (string)($this->course->destination_path ?? ''),
+ (string)($this->course->info['path'] ?? ''),
+ ])
+ );
+ }
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: rewriteLegacyCourseUrlsToAssets failed (tool_intro): '.$e->getMessage());
+ }
+
+ $out = \DocumentManager::replaceUrlWithNewCourseCode(
+ $html,
+ $this->course->code,
+ $this->course->destination_path,
+ $this->course->backup_path,
+ $this->course->info['path']
+ );
+ return $out === false ? '' : $out;
+ };
+
+ foreach ($resources[$bagKey] as $rawId => $tIntro) {
+ // prefer source->id only if non-empty AND not "0"; otherwise use the bag key ($rawId)
+ $toolKey = trim((string)($tIntro->id ?? ''));
+ if ($toolKey === '' || $toolKey === '0') {
+ $toolKey = (string)$rawId;
+ }
+
+ // normalize a couple of common aliases defensively
+ $alias = strtolower($toolKey);
+ if ($alias === 'homepage' || $alias === 'course_home') {
+ $toolKey = 'course_homepage';
+ }
+
+ // log exactly what we got to avoid future confusion
+ $this->dlog('restore_tool_intro: resolving tool key', [
+ 'raw_id' => (string)$rawId,
+ 'obj_id' => isset($tIntro->id) ? (string)$tIntro->id : null,
+ 'toolKey' => $toolKey,
+ ]);
+
+ $mapped = $tIntro->destination_id ?? 0;
+ if ($mapped > 0) {
+ $this->dlog('restore_tool_intro: already mapped, skipping', ['src_id' => $toolKey, 'dst_id' => $mapped]);
+ continue;
+ }
+
+ $introHtml = $rewriteContent($tIntro->intro_text ?? '');
+
+ // find core Tool by title (e.g., 'course_homepage')
+ $toolEntity = $toolRepo->findOneBy(['title' => $toolKey]);
+ if (!$toolEntity) {
+ $this->dlog('restore_tool_intro: missing Tool entity, skipping', ['tool' => $toolKey]);
+ continue;
+ }
+
+ // find or create the CTool row for this course+session+title
+ $cTool = $cToolRepo->findOneBy([
+ 'course' => $course,
+ 'session' => $session,
+ 'title' => $toolKey,
+ ]);
+
+ if (!$cTool) {
+ $cTool = (new CTool())
+ ->setTool($toolEntity)
+ ->setTitle($toolKey)
+ ->setCourse($course)
+ ->setSession($session)
+ ->setPosition(1)
+ ->setVisibility(true)
+ ->setParent($course)
+ ->setCreator($course->getCreator() ?? null)
+ ->addCourseLink($course);
+
+ $em->persist($cTool);
+ $em->flush();
+
+ $this->dlog('restore_tool_intro: CTool created', [
+ 'tool' => $toolKey,
+ 'ctool_id' => (int)$cTool->getIid(),
+ ]);
+ }
+
+ $intro = $introRepo->findOneBy(['courseTool' => $cTool]);
+
+ if ($intro) {
+ if ($this->file_option === FILE_SKIP) {
+ $this->dlog('restore_tool_intro: reuse existing (SKIP)', [
+ 'tool' => $toolKey,
+ 'intro_id' => (int)$intro->getIid(),
+ ]);
+ } else {
+ $intro->setIntroText($introHtml);
+ $em->persist($intro);
+ $em->flush();
+
+ $this->dlog('restore_tool_intro: intro overwritten', [
+ 'tool' => $toolKey,
+ 'intro_id' => (int)$intro->getIid(),
+ ]);
}
+ } else {
+ $intro = (new CToolIntro())
+ ->setCourseTool($cTool)
+ ->setIntroText($introHtml)
+ ->setParent($course);
+
+ $em->persist($intro);
+ $em->flush();
+
+ $this->dlog('restore_tool_intro: intro created', [
+ 'tool' => $toolKey,
+ 'intro_id' => (int)$intro->getIid(),
+ ]);
+ }
+
+ // map destination back into the legacy resource bag
+ if (!isset($this->course->resources[$bagKey][$rawId])) {
+ $this->course->resources[$bagKey][$rawId] = new \stdClass();
}
+ $this->course->resources[$bagKey][$rawId]->destination_id = (int)$intro->getIid();
}
+
+ $this->dlog('restore_tool_intro: end');
}
- /**
- * Restore a link-category.
- *
- * @param int $id
- * @param int $sessionId
- *
- * @return bool
- */
- public function restore_link_category($id, $sessionId = 0)
+
+ public function restore_events(int $sessionId = 0): void
{
- $params = [];
- $sessionId = (int) $sessionId;
- if (!empty($sessionId)) {
- $params['session_id'] = $sessionId;
+ if (!$this->course->has_resources(RESOURCE_EVENT)) {
+ return;
}
- if (0 == $id) {
- return 0;
- }
- $link_cat_table = Database::get_course_table(TABLE_LINK_CATEGORY);
- $resources = $this->course->resources;
- $link_cat = $resources[RESOURCE_LINKCATEGORY][$id];
- if (is_object($link_cat) && !$link_cat->is_restored()) {
- $sql = "SELECT MAX(display_order) FROM $link_cat_table
- WHERE c_id = ".$this->destination_course_id;
- $result = Database::query($sql);
- list($orderMax) = Database::fetch_array($result, 'NUM');
- $display_order = $orderMax + 1;
-
- $params['c_id'] = $this->destination_course_id;
- $params['category_title'] = self::DBUTF8($link_cat->title);
- $params['description'] = self::DBUTF8($link_cat->description);
- $params['display_order'] = $display_order;
- $new_id = Database::insert($link_cat_table, $params);
-
- if ($new_id) {
- $sql = "UPDATE $link_cat_table
- SET id = iid
- WHERE iid = $new_id";
- Database::query($sql);
-
- $courseInfo = api_get_course_info_by_id($this->destination_course_id);
- /*api_item_property_update(
- $courseInfo,
- TOOL_LINK_CATEGORY,
- $new_id,
- 'LinkCategoryAdded',
- api_get_user_id()
- );*/
- api_set_default_visibility(
- $new_id,
- TOOL_LINK_CATEGORY,
- 0,
- $courseInfo
- );
+ $resources = $this->course->resources ?? [];
+ $bag = $resources[RESOURCE_EVENT] ?? [];
+ $count = is_array($bag) ? count($bag) : 0;
+
+ $this->dlog('restore_events: begin', ['count' => $count]);
+
+ /** @var EntityManagerInterface $em */
+ $em = \Database::getManager();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity($sessionId);
+ $group = api_get_group_entity();
+ $eventRepo = Container::getCalendarEventRepository();
+ $attachRepo = Container::getCalendarEventAttachmentRepository();
+
+ // Content rewrite helper (prefer new helper if available)
+ $rewriteContent = function (?string $html): string {
+ $html = $html ?? '';
+ if ($html === '') {
+ return '';
+ }
+ try {
+ if (method_exists(ChamiloHelper::class, 'rewriteLegacyCourseUrlsToAssets')) {
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $html,
+ api_get_course_entity($this->destination_course_id),
+ $this->course->backup_path ?? '',
+ array_filter([
+ $this->course->destination_path ?? '',
+ (string) ($this->course->info['path'] ?? ''),
+ ])
+ );
+ }
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: rewriteLegacyCourseUrlsToAssets failed: '.$e->getMessage());
+ }
+
+ $out = \DocumentManager::replaceUrlWithNewCourseCode(
+ $html,
+ $this->course->code,
+ $this->course->destination_path,
+ $this->course->backup_path,
+ $this->course->info['path']
+ );
+
+ return $out === false ? '' : (string) $out;
+ };
+
+ // Dedupe by title inside same course/session (honor sameFileNameOption)
+ $findExistingByTitle = function (string $title) use ($eventRepo, $course, $session) {
+ $qb = $eventRepo->getResourcesByCourse($course, $session, null, null, true, true);
+ $qb->andWhere('resource.title = :t')->setParameter('t', $title)->setMaxResults(1);
+ return $qb->getQuery()->getOneOrNullResult();
+ };
+
+ // Attachment source in backup zip (calendar)
+ $originPath = rtrim((string)($this->course->backup_path ?? ''), '/').'/upload/calendar/';
+
+ foreach ($bag as $oldId => $raw) {
+ // Skip if already mapped to a positive destination id
+ $mapped = (int) ($raw->destination_id ?? 0);
+ if ($mapped > 0) {
+ $this->dlog('restore_events: already mapped, skipping', ['src_id' => (int)$oldId, 'dst_id' => $mapped]);
+ continue;
+ }
+
+ // Normalize input
+ $title = trim((string)($raw->title ?? ''));
+ if ($title === '') {
+ $title = 'Event';
+ }
+
+ $content = $rewriteContent((string)($raw->content ?? ''));
+
+ // Dates: accept various formats; allow empty endDate
+ $allDay = (bool)($raw->all_day ?? false);
+ $start = null;
+ $end = null;
+ try {
+ $s = (string)($raw->start_date ?? '');
+ if ($s !== '') { $start = new \DateTime($s); }
+ } catch (\Throwable $e) { $start = null; }
+ try {
+ $e = (string)($raw->end_date ?? '');
+ if ($e !== '') { $end = new \DateTime($e); }
+ } catch (\Throwable $e) { $end = null; }
+
+ // Dedupe policy
+ $existing = $findExistingByTitle($title);
+ if ($existing) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $destId = (int)$existing->getIid();
+ $this->course->resources[RESOURCE_EVENT][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_EVENT][$oldId]->destination_id = $destId;
+ $this->dlog('restore_events: reuse (SKIP)', [
+ 'src_id' => (int)$oldId, 'dst_id' => $destId, 'title' => $existing->getTitle()
+ ]);
+ // Try to add missing attachments (no duplicates by filename)
+ $this->restoreEventAttachments($raw, $existing, $originPath, $attachRepo, $em);
+ break;
+
+ case FILE_OVERWRITE:
+ $existing
+ ->setTitle($title)
+ ->setContent($content)
+ ->setAllDay($allDay)
+ ->setParent($course)
+ ->addCourseLink($course, $session, $group);
+
+ $existing->setStartDate($start);
+ $existing->setEndDate($end);
+
+ $em->persist($existing);
+ $em->flush();
+
+ $this->course->resources[RESOURCE_EVENT][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_EVENT][$oldId]->destination_id = (int)$existing->getIid();
+
+ $this->dlog('restore_events: overwrite', [
+ 'src_id' => (int)$oldId, 'dst_id' => (int)$existing->getIid(), 'title' => $title
+ ]);
+
+ $this->restoreEventAttachments($raw, $existing, $originPath, $attachRepo, $em);
+ break;
+
+ case FILE_RENAME:
+ default:
+ $base = $title;
+ $i = 1;
+ $candidate = $base;
+ while ($findExistingByTitle($candidate)) {
+ $i++;
+ $candidate = $base.' ('.$i.')';
+ }
+ $title = $candidate;
+ break;
+ }
}
- $this->course->resources[RESOURCE_LINKCATEGORY][$id]->destination_id = $new_id;
+ // Create new entity in course context
+ $entity = (new CCalendarEvent())
+ ->setTitle($title)
+ ->setContent($content)
+ ->setAllDay($allDay)
+ ->setParent($course)
+ ->addCourseLink($course, $session, $group);
+
+ $entity->setStartDate($start);
+ $entity->setEndDate($end);
+
+ $em->persist($entity);
+ $em->flush();
+
+ // Map new id
+ $destId = (int)$entity->getIid();
+ $this->course->resources[RESOURCE_EVENT][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_EVENT][$oldId]->destination_id = $destId;
- return $new_id;
+ $this->dlog('restore_events: created', ['src_id' => (int)$oldId, 'dst_id' => $destId, 'title' => $title]);
+
+ // Attachments (backup modern / legacy)
+ $this->restoreEventAttachments($raw, $entity, $originPath, $attachRepo, $em);
+
+ // (Optional) Repeat rules / reminders:
+ // If your backup exports recurrence/reminders, parse here and populate CCalendarEventRepeat / AgendaReminder.
+ // $this->restoreEventRecurrenceAndReminders($raw, $entity, $em);
}
- return $this->course->resources[RESOURCE_LINKCATEGORY][$id]->destination_id;
+ $this->dlog('restore_events: end');
}
- /**
- * Restore tool intro.
- *
- * @param int $sessionId
- */
- public function restore_tool_intro($sessionId = 0)
- {
- if ($this->course->has_resources(RESOURCE_TOOL_INTRO)) {
- $sessionId = (int) $sessionId;
- $tool_intro_table = Database::get_course_table(TABLE_TOOL_INTRO);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_TOOL_INTRO] as $id => $tool_intro) {
- if (!$this->copySessionContent) {
- $sql = "DELETE FROM $tool_intro_table
- WHERE
- c_id = ".$this->destination_course_id." AND
- id='".self::DBUTF8escapestring($tool_intro->id)."'";
- Database::query($sql);
+ private function restoreEventAttachments(
+ object $raw,
+ CCalendarEvent $entity,
+ string $originPath,
+ $attachRepo,
+ EntityManagerInterface $em
+ ): void {
+ // Helper to actually persist + move file
+ $persistAttachmentFromFile = function (string $src, string $filename, ?string $comment) use ($entity, $attachRepo, $em) {
+ if (!is_file($src) || !is_readable($src)) {
+ $this->dlog('restore_events: attachment source not readable', ['src' => $src]);
+ return;
+ }
+
+ // Avoid duplicate filenames on same event
+ foreach ($entity->getAttachments() as $att) {
+ if ($att->getFilename() === $filename) {
+ $this->dlog('restore_events: attachment already exists, skipping', ['filename' => $filename]);
+ return;
}
+ }
- $tool_intro->intro_text = DocumentManager::replaceUrlWithNewCourseCode(
- $tool_intro->intro_text,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
+ $attachment = (new CCalendarEventAttachment())
+ ->setFilename($filename)
+ ->setComment($comment ?? '')
+ ->setEvent($entity)
+ ->setParent($entity)
+ ->addCourseLink(
+ api_get_course_entity($this->destination_course_id),
+ api_get_session_entity(0),
+ api_get_group_entity()
);
- $params = [
- 'c_id' => $this->destination_course_id,
- 'id' => false === $tool_intro->id ? '' : self::DBUTF8($tool_intro->id),
- 'intro_text' => self::DBUTF8($tool_intro->intro_text),
- 'session_id' => $sessionId,
- ];
+ $em->persist($attachment);
+ $em->flush();
- $id = Database::insert($tool_intro_table, $params);
- if ($id) {
- if (!isset($this->course->resources[RESOURCE_TOOL_INTRO][$id])) {
- $this->course->resources[RESOURCE_TOOL_INTRO][$id] = new stdClass();
- }
+ if (method_exists($attachRepo, 'addFileFromLocalPath')) {
+ $attachRepo->addFileFromLocalPath($attachment, $src);
+ } else {
+ $dstDir = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/upload/calendar/';
+ @mkdir($dstDir, 0775, true);
+ $newName = uniqid('calendar_', true);
+ @copy($src, $dstDir.$newName);
+ }
- $this->course->resources[RESOURCE_TOOL_INTRO][$id]->destination_id = $id;
- }
+ $this->dlog('restore_events: attachment created', [
+ 'event_id' => (int)$entity->getIid(),
+ 'filename' => $filename,
+ ]);
+ };
+
+ // Case 1: modern backup fields on object
+ if (!empty($raw->attachment_path)) {
+ $src = rtrim($originPath, '/').'/'.$raw->attachment_path;
+ $filename = (string)($raw->attachment_filename ?? basename($src));
+ $comment = (string)($raw->attachment_comment ?? '');
+ $persistAttachmentFromFile($src, $filename, $comment);
+ return;
+ }
+
+ // Case 2: legacy lookup from old course tables when ->orig present
+ if (!empty($this->course->orig)) {
+ $table = \Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
+ $sql = 'SELECT path, comment, filename
+ FROM '.$table.'
+ WHERE c_id = '.$this->destination_course_id.'
+ AND agenda_id = '.(int)($raw->source_id ?? 0);
+ $res = \Database::query($sql);
+ while ($row = \Database::fetch_object($res)) {
+ $src = rtrim($originPath, '/').'/'.$row->path;
+ $persistAttachmentFromFile($src, (string)$row->filename, (string)$row->comment);
}
}
}
- /**
- * Restore events.
- *
- * @param int $sessionId
- */
- public function restore_events($sessionId = 0)
+ public function restore_course_descriptions($session_id = 0)
{
- if ($this->course->has_resources(RESOURCE_EVENT)) {
- $sessionId = (int) $sessionId;
- $table = Database::get_course_table(TABLE_AGENDA);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_EVENT] as $id => $event) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $event->content = DocumentManager::replaceUrlWithNewCourseCode(
- $event->content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ if (!$this->course->has_resources(RESOURCE_COURSEDESCRIPTION)) {
+ return;
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'title' => self::DBUTF8($event->title),
- 'content' => false === $event->content ? '' : self::DBUTF8($event->content),
- 'all_day' => $event->all_day,
- 'start_date' => $event->start_date,
- 'end_date' => $event->end_date,
- 'session_id' => $sessionId,
- ];
- $new_event_id = Database::insert($table, $params);
+ $resources = $this->course->resources;
+ $count = is_array($resources[RESOURCE_COURSEDESCRIPTION] ?? null)
+ ? count($resources[RESOURCE_COURSEDESCRIPTION])
+ : 0;
- if ($new_event_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $new_event_id";
- Database::query($sql);
+ $this->dlog('restore_course_descriptions: begin', ['count' => $count]);
- if (!isset($this->course->resources[RESOURCE_EVENT][$id])) {
- $this->course->resources[RESOURCE_EVENT][$id] = new stdClass();
- }
- $this->course->resources[RESOURCE_EVENT][$id]->destination_id = $new_event_id;
- }
-
- // Copy event attachment
- $origin_path = $this->course->backup_path.'/upload/calendar/';
- $destination_path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/upload/calendar/';
-
- if (!empty($this->course->orig)) {
- $table_attachment = Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
- $sql = 'SELECT path, comment, size, filename
- FROM '.$table_attachment.'
- WHERE c_id = '.$this->destination_course_id.' AND agenda_id = '.$id;
- $attachment_event = Database::query($sql);
- $attachment_event = Database::fetch_object($attachment_event);
-
- if (file_exists($origin_path.$attachment_event->path) &&
- !is_dir($origin_path.$attachment_event->path)
- ) {
- $new_filename = uniqid(''); //ass seen in the add_agenda_attachment_file() function in agenda.inc.php
- $copy_result = copy(
- $origin_path.$attachment_event->path,
- $destination_path.$new_filename
- );
- //$copy_result = true;
- if ($copy_result) {
- $table_attachment = Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
-
- $params = [
- 'c_id' => $this->destination_course_id,
- 'path' => self::DBUTF8($new_filename),
- 'comment' => self::DBUTF8($attachment_event->comment),
- 'size' => isset($attachment_event->size) ? $attachment_event->size : '',
- 'filename' => isset($attachment_event->filename) ? $attachment_event->filename : '',
- 'agenda_id' => $new_event_id,
- ];
- $id = Database::insert($table_attachment, $params);
- if ($id) {
- $sql = "UPDATE $table_attachment SET id = iid WHERE iid = $id";
- Database::query($sql);
- }
- }
- }
- } else {
- // get the info of the file
- if (!empty($event->attachment_path) &&
- is_file($origin_path.$event->attachment_path) &&
- is_readable($origin_path.$event->attachment_path)
- ) {
- $new_filename = uniqid(''); //ass seen in the add_agenda_attachment_file() function in agenda.inc.php
- $copy_result = copy(
- $origin_path.$event->attachment_path,
- $destination_path.$new_filename
- );
- if ($copy_result) {
- $table_attachment = Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
-
- $params = [
- 'c_id' => $this->destination_course_id,
- 'path' => self::DBUTF8($new_filename),
- 'comment' => self::DBUTF8($event->attachment_comment),
- 'size' => isset($event->size) ? $event->size : '',
- 'filename' => isset($event->filename) ? $event->filename : '',
- 'agenda_id' => $new_event_id,
- ];
- $id = Database::insert($table_attachment, $params);
-
- if ($id) {
- $sql = "UPDATE $table_attachment SET id = iid WHERE iid = $id";
- Database::query($sql);
- }
+ $em = \Database::getManager();
+ $repo = Container::getCourseDescriptionRepository();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity((int) $session_id);
+
+ $rewriteContent = function (string $html) use ($course) {
+ if ($html === '') {
+ return '';
+ }
+ if (method_exists(ChamiloHelper::class, 'rewriteLegacyCourseUrlsToAssets')) {
+ try {
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $html,
+ $course,
+ $this->course->backup_path ?? '',
+ array_filter([
+ $this->course->destination_path ?? '',
+ (string)($this->course->info['path'] ?? ''),
+ ])
+ );
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: rewriteLegacyCourseUrlsToAssets failed: '.$e->getMessage());
+ }
+ }
+ $out = \DocumentManager::replaceUrlWithNewCourseCode(
+ $html,
+ $this->course->code,
+ $this->course->destination_path,
+ $this->course->backup_path,
+ $this->course->info['path']
+ );
+
+ return $out === false ? '' : $out;
+ };
+
+ $findByTypeInCourse = function (int $type) use ($repo, $course, $session) {
+ if (method_exists($repo, 'findByTypeInCourse')) {
+ return $repo->findByTypeInCourse($type, $course, $session);
+ }
+ $qb = $repo->getResourcesByCourse($course, $session)->andWhere('resource.descriptionType = :t')->setParameter('t', $type);
+ return $qb->getQuery()->getResult();
+ };
+
+ $findByTitleInCourse = function (string $title) use ($repo, $course, $session) {
+ $qb = $repo->getResourcesByCourse($course, $session)
+ ->andWhere('resource.title = :t')
+ ->setParameter('t', $title)
+ ->setMaxResults(1);
+ return $qb->getQuery()->getOneOrNullResult();
+ };
+
+ foreach ($resources[RESOURCE_COURSEDESCRIPTION] as $oldId => $cd) {
+ $mapped = (int)($cd->destination_id ?? 0);
+ if ($mapped > 0) {
+ $this->dlog('restore_course_descriptions: already mapped, skipping', [
+ 'src_id' => (int)$oldId,
+ 'dst_id' => $mapped,
+ ]);
+ continue;
+ }
+
+ $rawTitle = (string)($cd->title ?? '');
+ $rawContent = (string)($cd->content ?? '');
+ $type = (int)($cd->description_type ?? CCourseDescription::TYPE_DESCRIPTION);
+ $title = trim($rawTitle) !== '' ? trim($rawTitle) : $rawTitle;
+ $content = $rewriteContent($rawContent);
+
+ $existingByType = $findByTypeInCourse($type);
+ $existingOne = $existingByType[0] ?? null;
+
+ if ($existingOne) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $destIid = (int)$existingOne->getIid();
+ $this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId]->destination_id = $destIid;
+
+ $this->dlog('restore_course_descriptions: reuse (SKIP)', [
+ 'src_id' => (int)$oldId,
+ 'dst_id' => $destIid,
+ 'type' => $type,
+ 'title' => (string)$existingOne->getTitle(),
+ ]);
+ break;
+
+ case FILE_OVERWRITE:
+ $existingOne
+ ->setTitle($title !== '' ? $title : (string)$existingOne->getTitle())
+ ->setContent($content)
+ ->setDescriptionType($type)
+ ->setProgress((int)($cd->progress ?? 0));
+ $existingOne->setParent($course)->addCourseLink($course, $session);
+
+ $em->persist($existingOne);
+ $em->flush();
+
+ $destIid = (int)$existingOne->getIid();
+ $this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId]->destination_id = $destIid;
+
+ $this->dlog('restore_course_descriptions: overwrite', [
+ 'src_id' => (int)$oldId,
+ 'dst_id' => $destIid,
+ 'type' => $type,
+ 'title' => (string)$existingOne->getTitle(),
+ ]);
+ break;
+
+ case FILE_RENAME:
+ default:
+ $base = $title !== '' ? $title : (string)($cd->extra['title'] ?? 'Description');
+ $i = 1;
+ $candidate = $base;
+ while ($findByTitleInCourse($candidate)) {
+ $i++;
+ $candidate = $base.' ('.$i.')';
}
- }
+ $title = $candidate;
+ break;
}
}
+
+ $entity = (new CCourseDescription())
+ ->setTitle($title)
+ ->setContent($content)
+ ->setDescriptionType($type)
+ ->setProgress((int)($cd->progress ?? 0))
+ ->setParent($course)
+ ->addCourseLink($course, $session);
+
+ $em->persist($entity);
+ $em->flush();
+
+ $destIid = (int)$entity->getIid();
+
+ if (!isset($this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId])) {
+ $this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId] = new \stdClass();
+ }
+ $this->course->resources[RESOURCE_COURSEDESCRIPTION][$oldId]->destination_id = $destIid;
+
+ $this->dlog('restore_course_descriptions: created', [
+ 'src_id' => (int)$oldId,
+ 'dst_id' => $destIid,
+ 'type' => $type,
+ 'title' => $title,
+ ]);
}
+
+ $this->dlog('restore_course_descriptions: end');
}
- /**
- * Restore course-description.
- *
- * @param int $session_id
- */
- public function restore_course_descriptions($session_id = 0)
+ private function resourceFileAbsPathFromAnnouncementAttachment(CAnnouncementAttachment $att): ?string
{
- if ($this->course->has_resources(RESOURCE_COURSEDESCRIPTION)) {
- $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_COURSEDESCRIPTION] as $id => $cd) {
- $courseDescription = (array) $cd;
+ $node = $att->getResourceNode();
+ if (!$node) return null;
- $content = isset($courseDescription['content']) ? $courseDescription['content'] : '';
- $descriptionType = isset($courseDescription['description_type']) ? $courseDescription['description_type'] : '';
- $title = isset($courseDescription['title']) ? $courseDescription['title'] : '';
+ $file = $node->getFirstResourceFile();
+ if (!$file) return null;
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $description_content = DocumentManager::replaceUrlWithNewCourseCode(
- $content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ /** @var ResourceNodeRepository $rnRepo */
+ $rnRepo = Container::$container->get(ResourceNodeRepository::class);
+ $rel = $rnRepo->getFilename($file);
+ if (!$rel) return null;
- $params = [];
- $session_id = (int) $session_id;
- $params['session_id'] = $session_id;
- $params['c_id'] = $this->destination_course_id;
- $params['description_type'] = self::DBUTF8($descriptionType);
- $params['title'] = self::DBUTF8($title);
- $params['content'] = false === $description_content ? '' : self::DBUTF8($description_content);
- $params['progress'] = 0;
-
- $id = Database::insert($table, $params);
- if ($id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $id";
- Database::query($sql);
-
- if (!isset($this->course->resources[RESOURCE_COURSEDESCRIPTION][$id])) {
- $this->course->resources[RESOURCE_COURSEDESCRIPTION][$id] = new stdClass();
- }
- $this->course->resources[RESOURCE_COURSEDESCRIPTION][$id]->destination_id = $id;
- }
- }
- }
+ $abs = $this->projectUploadBase().$rel;
+ return is_readable($abs) ? $abs : null;
}
- /**
- * Restore announcements.
- *
- * @param int $sessionId
- */
public function restore_announcements($sessionId = 0)
{
- if ($this->course->has_resources(RESOURCE_ANNOUNCEMENT)) {
- $sessionId = (int) $sessionId;
- $table = Database::get_course_table(TABLE_ANNOUNCEMENT);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_ANNOUNCEMENT] as $id => $announcement) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $announcement->content = DocumentManager::replaceUrlWithNewCourseCode(
- $announcement->content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ if (!$this->course->has_resources(RESOURCE_ANNOUNCEMENT)) {
+ return;
+ }
+
+ $sessionId = (int) $sessionId;
+ $resources = $this->course->resources;
+
+ $count = is_array($resources[RESOURCE_ANNOUNCEMENT] ?? null)
+ ? count($resources[RESOURCE_ANNOUNCEMENT])
+ : 0;
+
+ $this->dlog('restore_announcements: begin', ['count' => $count]);
+
+ /** @var EntityManagerInterface $em */
+ $em = \Database::getManager();
+ $course = api_get_course_entity($this->destination_course_id);
+ $session = api_get_session_entity($sessionId);
+ $group = api_get_group_entity();
+ $annRepo = Container::getAnnouncementRepository();
+ $attachRepo = Container::getAnnouncementAttachmentRepository();
+
+ $rewriteContent = function (string $html) {
+ if ($html === '') return '';
+ try {
+ if (class_exists(ChamiloHelper::class)
+ && method_exists(ChamiloHelper::class, 'rewriteLegacyCourseUrlsToAssets')) {
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $html,
+ api_get_course_entity($this->destination_course_id),
+ $this->course->backup_path ?? '',
+ array_filter([
+ $this->course->destination_path ?? '',
+ (string)($this->course->info['path'] ?? ''),
+ ])
+ );
+ }
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: rewriteLegacyCourseUrlsToAssets failed: '.$e->getMessage());
+ }
+
+ $out = \DocumentManager::replaceUrlWithNewCourseCode(
+ $html,
+ $this->course->code,
+ $this->course->destination_path,
+ $this->course->backup_path,
+ $this->course->info['path']
+ );
+
+ return $out === false ? '' : $out;
+ };
+
+ $findExistingByTitle = function (string $title) use ($annRepo, $course, $session) {
+ $qb = $annRepo->getResourcesByCourse($course, $session);
+ $qb->andWhere('resource.title = :t')->setParameter('t', $title)->setMaxResults(1);
+ return $qb->getQuery()->getOneOrNullResult();
+ };
+
+ $originPath = rtrim($this->course->backup_path ?? '', '/').'/upload/announcements/';
+
+ foreach ($resources[RESOURCE_ANNOUNCEMENT] as $oldId => $a) {
+ $mapped = (int)($a->destination_id ?? 0);
+ if ($mapped > 0) {
+ $this->dlog('restore_announcements: already mapped, skipping', [
+ 'src_id' => (int)$oldId, 'dst_id' => $mapped
+ ]);
+ continue;
+ }
+
+ $title = trim((string)($a->title ?? ''));
+ if ($title === '') { $title = 'Announcement'; }
+
+ $contentHtml = (string)($a->content ?? '');
+ $contentHtml = $rewriteContent($contentHtml);
+
+ $endDate = null;
+ try {
+ $rawDate = (string)($a->date ?? '');
+ if ($rawDate !== '') { $endDate = new \DateTime($rawDate); }
+ } catch (\Throwable $e) { $endDate = null; }
+
+ $emailSent = (bool)($a->email_sent ?? false);
+
+ $existing = $findExistingByTitle($title);
+ if ($existing) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $destId = (int)$existing->getIid();
+ $this->course->resources[RESOURCE_ANNOUNCEMENT][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_ANNOUNCEMENT][$oldId]->destination_id = $destId;
+ $this->dlog('restore_announcements: reuse (SKIP)', [
+ 'src_id' => (int)$oldId, 'dst_id' => $destId, 'title' => $existing->getTitle()
+ ]);
+ break;
+
+ case FILE_OVERWRITE:
+ $existing
+ ->setTitle($title)
+ ->setContent($contentHtml)
+ ->setParent($course)
+ ->addCourseLink($course, $session, $group)
+ ->setEmailSent($emailSent);
+ if ($endDate instanceof \DateTimeInterface) { $existing->setEndDate($endDate); }
+ $em->persist($existing);
+ $em->flush();
+
+ $this->course->resources[RESOURCE_ANNOUNCEMENT][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_ANNOUNCEMENT][$oldId]->destination_id = (int)$existing->getIid();
+
+ $this->dlog('restore_announcements: overwrite', [
+ 'src_id' => (int)$oldId, 'dst_id' => (int)$existing->getIid(), 'title' => $title
+ ]);
+
+ $this->restoreAnnouncementAttachments($a, $existing, $originPath, $attachRepo, $em);
+ continue 2;
+
+ case FILE_RENAME:
+ default:
+ $base = $title; $i = 1; $candidate = $base;
+ while ($findExistingByTitle($candidate)) { $i++; $candidate = $base.' ('.$i.')'; }
+ $title = $candidate;
+ break;
+ }
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'title' => self::DBUTF8($announcement->title),
- 'content' => false === $announcement->content ? '' : self::DBUTF8($announcement->content),
- 'end_date' => $announcement->date,
- 'display_order' => $announcement->display_order,
- 'email_sent' => $announcement->email_sent,
- 'session_id' => $sessionId,
- ];
+ $entity = (new CAnnouncement())
+ ->setTitle($title)
+ ->setContent($contentHtml)
+ ->setParent($course)
+ ->addCourseLink($course, $session, $group)
+ ->setEmailSent($emailSent);
+ if ($endDate instanceof \DateTimeInterface) { $entity->setEndDate($endDate); }
- $new_announcement_id = Database::insert($table, $params);
+ $em->persist($entity);
+ $em->flush();
- if ($new_announcement_id) {
- $sql = "UPDATE $table SET id = iid WHERE iid = $new_announcement_id";
- Database::query($sql);
+ $destId = (int)$entity->getIid();
+ $this->course->resources[RESOURCE_ANNOUNCEMENT][$oldId] ??= new \stdClass();
+ $this->course->resources[RESOURCE_ANNOUNCEMENT][$oldId]->destination_id = $destId;
- if (!isset($this->course->resources[RESOURCE_ANNOUNCEMENT][$id])) {
- $this->course->resources[RESOURCE_ANNOUNCEMENT][$id] = new stdClass();
- }
- $this->course->resources[RESOURCE_ANNOUNCEMENT][$id]->destination_id = $new_announcement_id;
- }
-
- $origin_path = $this->course->backup_path.'/upload/announcements/';
- $destination_path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/upload/announcements/';
-
- // Copy announcement attachment file
- if (!empty($this->course->orig)) {
- $table_attachment = Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT);
- $sql = 'SELECT path, comment, size, filename
- FROM '.$table_attachment.'
- WHERE
- c_id = '.$this->destination_course_id.' AND
- announcement_id = '.$id;
- $attachment_event = Database::query($sql);
- $attachment_event = Database::fetch_object($attachment_event);
-
- if (file_exists($origin_path.$attachment_event->path) &&
- !is_dir($origin_path.$attachment_event->path)
- ) {
- $new_filename = uniqid(''); //ass seen in the add_agenda_attachment_file() function in agenda.inc.php
- $copy_result = copy(
- $origin_path.$attachment_event->path,
- $destination_path.$new_filename
- );
+ $this->dlog('restore_announcements: created', [
+ 'src_id' => (int)$oldId, 'dst_id' => $destId, 'title' => $title
+ ]);
+
+ $this->restoreAnnouncementAttachments($a, $entity, $originPath, $attachRepo, $em);
+ }
+
+ $this->dlog('restore_announcements: end');
+ }
- if ($copy_result) {
- $table_attachment = Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT);
+ private function restoreAnnouncementAttachments(
+ object $a,
+ CAnnouncement $entity,
+ string $originPath,
+ $attachRepo,
+ EntityManagerInterface $em
+ ): void {
+ $copyMode = empty($this->course->backup_path);
+
+ if ($copyMode) {
+ $srcAttachmentIds = [];
+ if (!empty($a->attachment_source_id)) { $srcAttachmentIds[] = (int)$a->attachment_source_id; }
+ if (!empty($a->attachment_source_ids) && is_array($a->attachment_source_ids)) {
+ foreach ($a->attachment_source_ids as $sid) { $sid = (int)$sid; if ($sid > 0) $srcAttachmentIds[] = $sid; }
+ }
+ if (empty($srcAttachmentIds) && !empty($a->source_id)) {
+ $srcAnn = Container::getAnnouncementRepository()->find((int)$a->source_id);
+ if ($srcAnn) {
+ $srcAtts = Container::getAnnouncementAttachmentRepository()->findBy(['announcement' => $srcAnn]);
+ foreach ($srcAtts as $sa) { $srcAttachmentIds[] = (int)$sa->getIid(); }
+ }
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'path' => self::DBUTF8($new_filename),
- 'comment' => self::DBUTF8($attachment_event->comment),
- 'size' => $attachment_event->size,
- 'filename' => $attachment_event->filename,
- 'announcement_id' => $new_announcement_id,
- ];
+ if (!empty($srcAttachmentIds)) {
+ $attRepo = Container::getAnnouncementAttachmentRepository();
- $attachmentId = Database::insert($table_attachment, $params);
+ foreach (array_unique($srcAttachmentIds) as $sid) {
+ /** @var CAnnouncementAttachment|null $srcAtt */
+ $srcAtt = $attRepo->find($sid);
+ if (!$srcAtt) { continue; }
- if ($attachmentId) {
- $sql = "UPDATE $table_attachment SET id = iid WHERE iid = $attachmentId";
- Database::query($sql);
- }
- }
+ $abs = $this->resourceFileAbsPathFromAnnouncementAttachment($srcAtt);
+ if (!$abs) {
+ $this->dlog('restore_announcements: source attachment file not readable', ['src_att_id' => $sid]);
+ continue;
}
- } else {
- // get the info of the file
- if (!empty($announcement->attachment_path) &&
- is_file($origin_path.$announcement->attachment_path) &&
- is_readable($origin_path.$announcement->attachment_path)
- ) {
- $new_filename = uniqid(''); //ass seen in the add_agenda_attachment_file() function in agenda.inc.php
- $copy_result = copy($origin_path.$announcement->attachment_path, $destination_path.$new_filename);
-
- if ($copy_result) {
- $table_attachment = Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT);
-
- $params = [
- 'c_id' => $this->destination_course_id,
- 'path' => self::DBUTF8($new_filename),
- 'comment' => self::DBUTF8($announcement->attachment_comment),
- 'size' => $announcement->attachment_size,
- 'filename' => $announcement->attachment_filename,
- 'announcement_id' => $new_announcement_id,
- ];
-
- $attachmentId = Database::insert($table_attachment, $params);
-
- if ($attachmentId) {
- $sql = "UPDATE $table_attachment SET id = iid WHERE iid = $attachmentId";
- Database::query($sql);
+
+ $filename = $srcAtt->getFilename() ?: basename($abs);
+ foreach ($entity->getAttachments() as $existingA) {
+ if ($existingA->getFilename() === $filename) {
+ if ($this->file_option === FILE_SKIP) { continue 2; }
+ if ($this->file_option === FILE_RENAME) {
+ $pi = pathinfo($filename);
+ $base = $pi['filename'] ?? $filename;
+ $ext = isset($pi['extension']) && $pi['extension'] !== '' ? ('.'.$pi['extension']) : '';
+ $i = 1; $candidate = $filename;
+ $existingNames = array_map(fn($x) => $x->getFilename(), iterator_to_array($entity->getAttachments()));
+ while (in_array($candidate, $existingNames, true)) { $candidate = $base.'_'.$i.$ext; $i++; }
+ $filename = $candidate;
}
}
}
+
+ $newAtt = (new CAnnouncementAttachment())
+ ->setFilename($filename)
+ ->setComment((string)$srcAtt->getComment())
+ ->setSize((int)$srcAtt->getSize())
+ ->setPath(uniqid('announce_', true))
+ ->setAnnouncement($entity)
+ ->setParent($entity)
+ ->addCourseLink(
+ api_get_course_entity($this->destination_course_id),
+ api_get_session_entity(0),
+ api_get_group_entity()
+ );
+
+ $em->persist($newAtt);
+ $em->flush();
+
+ if (method_exists($attachRepo, 'addFileFromLocalPath')) {
+ $attachRepo->addFileFromLocalPath($newAtt, $abs);
+ } else {
+ $tmp = tempnam(sys_get_temp_dir(), 'ann_');
+ @copy($abs, $tmp);
+ $_FILES['user_upload'] = [
+ 'name' => $filename,
+ 'type' => function_exists('mime_content_type') ? (mime_content_type($tmp) ?: 'application/octet-stream') : 'application/octet-stream',
+ 'tmp_name' => $tmp,
+ 'error' => 0,
+ 'size' => filesize($tmp) ?: (int)$srcAtt->getSize(),
+ ];
+ $attachRepo->addFileFromFileRequest($newAtt, 'user_upload');
+ @unlink($tmp);
+ }
+
+ $this->dlog('restore_announcements: attachment copied from ResourceFile', [
+ 'dst_announcement_id' => (int)$entity->getIid(),
+ 'filename' => $newAtt->getFilename(),
+ 'size' => $newAtt->getSize(),
+ ]);
+ }
+ }
+ return;
+ }
+
+ $meta = null;
+ if (!empty($a->attachment_path)) {
+ $src = rtrim($originPath, '/').'/'.$a->attachment_path;
+ if (is_file($src) && is_readable($src)) {
+ $meta = [
+ 'src' => $src,
+ 'filename' => (string)($a->attachment_filename ?? basename($src)),
+ 'comment' => (string)($a->attachment_comment ?? ''),
+ 'size' => (int)($a->attachment_size ?? (filesize($src) ?: 0)),
+ ];
+ }
+ }
+ if (!$meta && !empty($this->course->orig)) {
+ $table = \Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT);
+ $sql = 'SELECT path, comment, size, filename
+ FROM '.$table.'
+ WHERE c_id = '.$this->destination_course_id.'
+ AND announcement_id = '.(int)($a->source_id ?? 0);
+ $res = \Database::query($sql);
+ if ($row = \Database::fetch_object($res)) {
+ $src = rtrim($originPath, '/').'/'.$row->path;
+ if (is_file($src) && is_readable($src)) {
+ $meta = [
+ 'src' => $src,
+ 'filename' => (string)$row->filename,
+ 'comment' => (string)$row->comment,
+ 'size' => (int)$row->size,
+ ];
}
}
}
+ if (!$meta) { return; }
+
+ $attachment = (new CAnnouncementAttachment())
+ ->setFilename($meta['filename'])
+ ->setPath(uniqid('announce_', true))
+ ->setComment($meta['comment'])
+ ->setSize($meta['size'])
+ ->setAnnouncement($entity)
+ ->setParent($entity)
+ ->addCourseLink(
+ api_get_course_entity($this->destination_course_id),
+ api_get_session_entity(0),
+ api_get_group_entity()
+ );
+
+ $em->persist($attachment);
+ $em->flush();
+
+ $tmp = tempnam(sys_get_temp_dir(), 'ann_');
+ @copy($meta['src'], $tmp);
+ $_FILES['user_upload'] = [
+ 'name' => $meta['filename'],
+ 'type' => function_exists('mime_content_type') ? (mime_content_type($tmp) ?: 'application/octet-stream') : 'application/octet-stream',
+ 'tmp_name' => $tmp,
+ 'error' => 0,
+ 'size' => filesize($tmp) ?: $meta['size'],
+ ];
+ $attachRepo->addFileFromFileRequest($attachment, 'user_upload');
+ @unlink($tmp);
+
+ $this->dlog('restore_announcements: attachment stored (ZIP)', [
+ 'announcement_id' => (int)$entity->getIid(),
+ 'filename' => $attachment->getFilename(),
+ 'size' => $attachment->getSize(),
+ ]);
}
- /**
- * Restore Quiz.
- *
- * @param int $session_id
- * @param bool $respect_base_content
- */
- public function restore_quizzes(
- $session_id = 0,
- $respect_base_content = false
- ) {
- if ($this->course->has_resources(RESOURCE_QUIZ)) {
- $table_qui = Database::get_course_table(TABLE_QUIZ_TEST);
- $table_rel = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
- $table_doc = Database::get_course_table(TABLE_DOCUMENT);
- $resources = $this->course->resources;
+ public function restore_quizzes($session_id = 0, $respect_base_content = false)
+ {
+ if (!$this->course->has_resources(RESOURCE_QUIZ)) {
+ error_log('RESTORE_QUIZ: No quiz resources in backup.');
+ return;
+ }
- foreach ($resources[RESOURCE_QUIZ] as $id => $quiz) {
- if (isset($quiz->obj)) {
- // For new imports
- $quiz = $quiz->obj;
- } else {
- // For backward compatibility
- $quiz->obj = $quiz;
- }
-
- $doc = '';
- if (!empty($quiz->sound)) {
- if (isset($this->course->resources[RESOURCE_DOCUMENT][$quiz->sound]) &&
- $this->course->resources[RESOURCE_DOCUMENT][$quiz->sound]->is_restored()) {
- $sql = "SELECT path FROM $table_doc
- WHERE
- c_id = ".$this->destination_course_id.' AND
- id = '.$resources[RESOURCE_DOCUMENT][$quiz->sound]->destination_id;
- $doc = Database::query($sql);
- $doc = Database::fetch_object($doc);
- $doc = str_replace('/audio/', '', $doc->path);
- }
+ $em = Database::getManager();
+ $resources = $this->course->resources;
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ $sessionEntity = !empty($session_id) ? api_get_session_entity((int)$session_id) : api_get_session_entity();
+
+ $rewrite = function (?string $html) use ($courseEntity) {
+ if ($html === null || $html === false) return '';
+ if (class_exists(ChamiloHelper::class)
+ && method_exists(ChamiloHelper::class, 'rewriteLegacyCourseUrlsToAssets')) {
+ try {
+ $backupRoot = $this->course->backup_path ?? '';
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets($html, $courseEntity, $backupRoot);
+ } catch (\Throwable $e) {
+ error_log('RESTORE_QUIZ: rewriteLegacyCourseUrlsToAssets failed: '.$e->getMessage());
+ return $html;
}
+ }
+ return $html;
+ };
- if (-1 != $id) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $quiz->description = DocumentManager::replaceUrlWithNewCourseCode(
- $quiz->description,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ if (empty($this->course->resources[RESOURCE_QUIZQUESTION])
+ && !empty($this->course->resources['Exercise_Question'])) {
+ $this->course->resources[RESOURCE_QUIZQUESTION] = $this->course->resources['Exercise_Question'];
+ $resources = $this->course->resources;
+ error_log('RESTORE_QUIZ: Aliased Exercise_Question -> RESOURCE_QUIZQUESTION for restore.');
+ }
- $quiz->start_time = '0000-00-00 00:00:00' == $quiz->start_time ? null : $quiz->start_time;
- $quiz->end_time = '0000-00-00 00:00:00' == $quiz->end_time ? null : $quiz->end_time;
+ foreach ($resources[RESOURCE_QUIZ] as $id => $quizWrap) {
+ $quiz = isset($quizWrap->obj) ? $quizWrap->obj : $quizWrap;
- global $_custom;
- if (isset($_custom['exercises_clean_dates_when_restoring']) &&
- $_custom['exercises_clean_dates_when_restoring']
- ) {
- $quiz->start_time = null;
- $quiz->end_time = null;
- }
+ $description = $rewrite($quiz->description ?? '');
+ $quiz->start_time = ($quiz->start_time === '0000-00-00 00:00:00') ? null : ($quiz->start_time ?? null);
+ $quiz->end_time = ($quiz->end_time === '0000-00-00 00:00:00') ? null : ($quiz->end_time ?? null);
- $params = [
- 'c_id' => $this->destination_course_id,
- 'title' => self::DBUTF8($quiz->title),
- 'description' => false === $quiz->description ? '' : self::DBUTF8($quiz->description),
- 'type' => isset($quiz->quiz_type) ? (int) $quiz->quiz_type : $quiz->type,
- 'random' => (int) $quiz->random,
- 'active' => $quiz->active,
- 'sound' => self::DBUTF8($doc),
- 'max_attempt' => (int) $quiz->max_attempt,
- 'results_disabled' => (int) $quiz->results_disabled,
- 'access_condition' => $quiz->access_condition,
- 'pass_percentage' => $quiz->pass_percentage,
- 'feedback_type' => (int) $quiz->feedback_type,
- 'random_answers' => (int) $quiz->random_answers,
- 'random_by_category' => (int) $quiz->random_by_category,
- 'review_answers' => (int) $quiz->review_answers,
- 'propagate_neg' => (int) $quiz->propagate_neg,
- 'text_when_finished' => (string) $quiz->text_when_finished,
- 'text_when_finished_failure' => (string) $quiz->text_when_finished_failure,
- 'expired_time' => (int) $quiz->expired_time,
- 'start_time' => $quiz->start_time,
- 'end_time' => $quiz->end_time,
- 'save_correct_answers' => 0,
- 'display_category_name' => 0,
- 'save_correct_answers' => isset($quiz->save_correct_answers) ? $quiz->save_correct_answers : 0,
- 'hide_question_title' => isset($quiz->hide_question_title) ? $quiz->hide_question_title : 0,
- ];
+ global $_custom;
+ if (!empty($_custom['exercises_clean_dates_when_restoring'])) {
+ $quiz->start_time = null;
+ $quiz->end_time = null;
+ }
- $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
- if ($allow) {
- $params['notifications'] = isset($quiz->notifications) ? $quiz->notifications : '';
- }
+ if ((int)$id === -1) {
+ $this->course->resources[RESOURCE_QUIZ][$id]->destination_id = -1;
+ error_log('RESTORE_QUIZ: Skipping virtual quiz (id=-1).');
+ continue;
+ }
- if ($respect_base_content) {
- $my_session_id = $quiz->session_id;
- if (!empty($quiz->session_id)) {
- $my_session_id = $session_id;
- }
- $params['session_id'] = $my_session_id;
- } else {
- if (!empty($session_id)) {
- $session_id = (int) $session_id;
- $params['session_id'] = $session_id;
- }
- }
- $new_id = Database::insert($table_qui, $params);
+ $entity = (new CQuiz())
+ ->setParent($courseEntity)
+ ->addCourseLink(
+ $courseEntity,
+ $respect_base_content ? $sessionEntity : (!empty($session_id) ? $sessionEntity : api_get_session_entity()),
+ api_get_group_entity()
+ )
+ ->setTitle((string) $quiz->title)
+ ->setDescription($description)
+ ->setType(isset($quiz->quiz_type) ? (int) $quiz->quiz_type : (int) $quiz->type)
+ ->setRandom((int) $quiz->random)
+ ->setRandomAnswers((bool) $quiz->random_answers)
+ ->setResultsDisabled((int) $quiz->results_disabled)
+ ->setMaxAttempt((int) $quiz->max_attempt)
+ ->setFeedbackType((int) $quiz->feedback_type)
+ ->setExpiredTime((int) $quiz->expired_time)
+ ->setReviewAnswers((int) $quiz->review_answers)
+ ->setRandomByCategory((int) $quiz->random_by_category)
+ ->setTextWhenFinished((string) ($quiz->text_when_finished ?? ''))
+ ->setTextWhenFinishedFailure((string) ($quiz->text_when_finished_failure ?? ''))
+ ->setDisplayCategoryName((int) ($quiz->display_category_name ?? 0))
+ ->setSaveCorrectAnswers(isset($quiz->save_correct_answers) ? (int) $quiz->save_correct_answers : 0)
+ ->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);
+
+ if (isset($quiz->access_condition) && $quiz->access_condition !== '') {
+ $entity->setAccessCondition((string)$quiz->access_condition);
+ }
+ if (isset($quiz->pass_percentage) && $quiz->pass_percentage !== '' && $quiz->pass_percentage !== null) {
+ $entity->setPassPercentage((int)$quiz->pass_percentage);
+ }
+ if (isset($quiz->question_selection_type) && $quiz->question_selection_type !== '' && $quiz->question_selection_type !== null) {
+ $entity->setQuestionSelectionType((int)$quiz->question_selection_type);
+ }
+ if ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise')) {
+ $entity->setNotifications((string)($quiz->notifications ?? ''));
+ }
+
+ $em->persist($entity);
+ $em->flush();
+
+ $newQuizId = (int)$entity->getIid();
+ $this->course->resources[RESOURCE_QUIZ][$id]->destination_id = $newQuizId;
+
+ $qCount = isset($quiz->question_ids) ? count((array)$quiz->question_ids) : 0;
+ error_log('RESTORE_QUIZ: Created quiz iid='.$newQuizId.' title="'.(string)$quiz->title.'" with '.$qCount.' question ids.');
- if ($new_id) {
- $sql = "UPDATE $table_qui SET id = iid WHERE iid = $new_id";
- Database::query($sql);
+ $order = 0;
+ if (!empty($quiz->question_ids)) {
+ foreach ($quiz->question_ids as $index => $question_id) {
+ $qid = $this->restore_quiz_question($question_id);
+ if (!$qid) {
+ error_log('RESTORE_QUIZ: restore_quiz_question returned 0 for src_question_id='.$question_id);
+ continue;
}
- } else {
- // $id = -1 identifies the fictionary test for collecting
- // orphan questions. We do not store it in the database.
- $new_id = -1;
- }
-
- $this->course->resources[RESOURCE_QUIZ][$id]->destination_id = $new_id;
- $order = 0;
- if (!empty($quiz->question_ids)) {
- foreach ($quiz->question_ids as $index => $question_id) {
- $qid = $this->restore_quiz_question($question_id);
- $question_order = $quiz->question_orders[$index] ?: $order;
- $order++;
- $sql = "INSERT IGNORE INTO $table_rel SET
- c_id = ".$this->destination_course_id.",
- question_id = $qid ,
- quiz_id = $new_id ,
- question_order = ".$question_order;
- Database::query($sql);
+
+ $question_order = !empty($quiz->question_orders[$index])
+ ? (int)$quiz->question_orders[$index]
+ : $order;
+
+ $order++;
+
+ $questionEntity = $em->getRepository(CQuizQuestion::class)->find($qid);
+ if (!$questionEntity) {
+ error_log('RESTORE_QUIZ: Question entity not found after insert. qid='.$qid);
+ continue;
}
+
+ $rel = (new CQuizRelQuestion())
+ ->setQuiz($entity)
+ ->setQuestion($questionEntity)
+ ->setQuestionOrder($question_order);
+
+ $em->persist($rel);
+ $em->flush();
}
+ } else {
+ error_log('RESTORE_QUIZ: No questions bound to quiz src_id='.$id.' (title="'.(string)$quiz->title.'").');
}
}
}
+
/**
- * Restore quiz-questions.
- *
- * @params int $id question id
+ * Restore quiz-questions. Returns new question IID.
*/
public function restore_quiz_question($id)
{
- $em = Database::getManager();
+ $em = Database::getManager();
$resources = $this->course->resources;
- /** @var QuizQuestion $question */
- $question = isset($resources[RESOURCE_QUIZQUESTION][$id]) ? $resources[RESOURCE_QUIZQUESTION][$id] : null;
- $new_id = 0;
- if (is_object($question)) {
- if ($question->is_restored()) {
- return $question->destination_id;
- }
- $table_que = Database::get_course_table(TABLE_QUIZ_QUESTION);
- $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
- $table_options = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
+ if (empty($resources[RESOURCE_QUIZQUESTION]) && !empty($resources['Exercise_Question'])) {
+ $resources[RESOURCE_QUIZQUESTION] = $this->course->resources[RESOURCE_QUIZQUESTION]
+ = $this->course->resources['Exercise_Question'];
+ error_log('RESTORE_QUESTION: Aliased Exercise_Question -> RESOURCE_QUIZQUESTION for restore.');
+ }
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $question->description = DocumentManager::replaceUrlWithNewCourseCode(
- $question->description,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ /** @var object|null $question */
+ $question = $resources[RESOURCE_QUIZQUESTION][$id] ?? null;
+ if (!is_object($question)) {
+ error_log('RESTORE_QUESTION: Question not found in resources. src_id='.$id);
+ return 0;
+ }
+ if (method_exists($question, 'is_restored') && $question->is_restored()) {
+ return (int)$question->destination_id;
+ }
- $imageNewId = '';
- if (preg_match('/^quiz-.*$/', $question->picture) &&
- isset($resources[RESOURCE_DOCUMENT]['image_quiz'][$question->picture])
- ) {
- $imageNewId = $resources[RESOURCE_DOCUMENT]['image_quiz'][$question->picture]['destination_id'];
- } else {
- if (isset($resources[RESOURCE_DOCUMENT][$question->picture])) {
- $documentsToRestore = $resources[RESOURCE_DOCUMENT][$question->picture];
- $imageNewId = $documentsToRestore->destination_id;
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+
+ $rewrite = function (?string $html) use ($courseEntity) {
+ if ($html === null || $html === false) return '';
+ if (class_exists(ChamiloHelper::class)
+ && method_exists(ChamiloHelper::class, 'rewriteLegacyCourseUrlsToAssets')) {
+ try {
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)$html, $courseEntity, null);
+ } catch (\ArgumentCountError $e) {
+ return ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)$html, $courseEntity);
+ } catch (\Throwable $e) {
+ error_log('RESTORE_QUESTION: rewriteLegacyCourseUrlsToAssets failed: '.$e->getMessage());
+ return $html;
}
}
- $question->question = DocumentManager::replaceUrlWithNewCourseCode(
- $question->question,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- $params = [
- 'c_id' => $this->destination_course_id,
- 'question' => self::DBUTF8($question->question),
- 'description' => false === $question->description ? '' : self::DBUTF8($question->description),
- 'ponderation' => self::DBUTF8($question->ponderation),
- 'position' => self::DBUTF8($question->position),
- 'type' => self::DBUTF8($question->quiz_type),
- 'picture' => self::DBUTF8($imageNewId),
- 'level' => self::DBUTF8($question->level),
- 'extra' => self::DBUTF8($question->extra),
- ];
+ return $html;
+ };
+
+ $question->description = $rewrite($question->description ?? '');
+ $question->question = $rewrite($question->question ?? '');
+
+ $imageNewId = '';
+ if (!empty($question->picture)) {
+ if (isset($resources[RESOURCE_DOCUMENT]['image_quiz'][$question->picture])) {
+ $imageNewId = (string) $resources[RESOURCE_DOCUMENT]['image_quiz'][$question->picture]['destination_id'];
+ } elseif (isset($resources[RESOURCE_DOCUMENT][$question->picture])) {
+ $imageNewId = (string) $resources[RESOURCE_DOCUMENT][$question->picture]->destination_id;
+ }
+ }
- $new_id = Database::insert($table_que, $params);
+ $qType = (int) ($question->quiz_type ?? $question->type);
+ $entity = (new CQuizQuestion())
+ ->setParent($courseEntity)
+ ->addCourseLink($courseEntity, api_get_session_entity(), api_get_group_entity())
+ ->setQuestion($question->question)
+ ->setDescription($question->description)
+ ->setPonderation((float) ($question->ponderation ?? 0))
+ ->setPosition((int) ($question->position ?? 1))
+ ->setType($qType)
+ ->setPicture($imageNewId)
+ ->setLevel((int) ($question->level ?? 1))
+ ->setExtra((string) ($question->extra ?? ''));
+
+ $em->persist($entity);
+ $em->flush();
+
+ $new_id = (int)$entity->getIid();
+ if (!$new_id) {
+ error_log('RESTORE_QUESTION: Failed to obtain new question iid for src_id='.$id);
+ return 0;
+ }
- if ($new_id) {
- $sql = "UPDATE $table_que SET id = iid WHERE iid = $new_id";
- Database::query($sql);
- } else {
- return 0;
- }
-
- $correctAnswers = [];
- $allAnswers = [];
- $onlyAnswers = [];
-
- if (in_array($question->quiz_type, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE])) {
- $tempAnswerList = $question->answers;
- foreach ($tempAnswerList as &$value) {
- $value['answer'] = DocumentManager::replaceUrlWithNewCourseCode(
- $value['answer'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- }
- $allAnswers = array_column($tempAnswerList, 'answer', 'id');
- }
+ $answers = (array)($question->answers ?? []);
+ error_log('RESTORE_QUESTION: Creating question src_id='.$id.' dst_iid='.$new_id.' answers_count='.count($answers));
- if (in_array($question->quiz_type, [MATCHING, MATCHING_DRAGGABLE])) {
- $temp = [];
- foreach ($question->answers as $index => $answer) {
- $temp[$answer['position']] = $answer;
- }
+ $isMatchingFamily = in_array($qType, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE], true);
+ $correctMapSrcToDst = []; // dstAnsId => srcCorrectRef
+ $allSrcAnswersById = []; // srcAnsId => text
+ $dstAnswersByIdText = []; // dstAnsId => text
- foreach ($temp as $index => $answer) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $answer['answer'] = DocumentManager::replaceUrlWithNewCourseCode(
- $answer['answer'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ if ($isMatchingFamily) {
+ foreach ($answers as $a) {
+ $allSrcAnswersById[$a['id']] = $rewrite($a['answer'] ?? '');
+ }
+ }
- $answer['comment'] = DocumentManager::replaceUrlWithNewCourseCode(
- $answer['comment'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ foreach ($answers as $a) {
+ $ansText = $rewrite($a['answer'] ?? '');
+ $comment = $rewrite($a['comment'] ?? '');
+
+ $ans = (new CQuizAnswer())
+ ->setQuestion($entity)
+ ->setAnswer((string)$ansText)
+ ->setComment((string)$comment)
+ ->setPonderation((float)($a['ponderation'] ?? 0))
+ ->setPosition((int)($a['position'] ?? 0))
+ ->setHotspotCoordinates(isset($a['hotspot_coordinates']) ? (string)$a['hotspot_coordinates'] : null)
+ ->setHotspotType(isset($a['hotspot_type']) ? (string)$a['hotspot_type'] : null);
+
+ if (isset($a['correct']) && $a['correct'] !== '' && $a['correct'] !== null) {
+ $ans->setCorrect((int)$a['correct']);
+ }
- $quizAnswer = new CQuizAnswer();
- $quizAnswer
- ->setCId($this->destination_course_id)
- ->setQuestionId($new_id)
- ->setAnswer(self::DBUTF8($answer['answer']))
- ->setCorrect($answer['correct'])
- ->setComment(false === $answer['comment'] ? '' : self::DBUTF8($answer['comment']))
- ->setPonderation($answer['ponderation'])
- ->setPosition($answer['position'])
- ->setHotspotCoordinates($answer['hotspot_coordinates'])
- ->setHotspotType($answer['hotspot_type']);
-
- $em->persist($quizAnswer);
- $em->flush();
+ $em->persist($ans);
+ $em->flush();
- $answerId = $quizAnswer->getIid();
+ if ($isMatchingFamily) {
+ $correctMapSrcToDst[(int)$ans->getIid()] = $a['correct'] ?? null;
+ $dstAnswersByIdText[(int)$ans->getIid()] = $ansText;
+ }
+ }
- if ($answerId) {
- $correctAnswers[$answerId] = $answer['correct'];
- $onlyAnswers[$answerId] = $answer['answer'];
+ if ($isMatchingFamily && $correctMapSrcToDst) {
+ foreach ($entity->getAnswers() as $dstAns) {
+ $dstAid = (int)$dstAns->getIid();
+ $srcRef = $correctMapSrcToDst[$dstAid] ?? null;
+ if ($srcRef === null) continue;
+
+ if (isset($allSrcAnswersById[$srcRef])) {
+ $needle = $allSrcAnswersById[$srcRef];
+ $newDst = null;
+ foreach ($dstAnswersByIdText as $candId => $txt) {
+ if ($txt === $needle) { $newDst = $candId; break; }
+ }
+ if ($newDst !== null) {
+ $dstAns->setCorrect((int)$newDst);
+ $em->persist($dstAns);
}
}
- } else {
- foreach ($question->answers as $index => $answer) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $answer['answer'] = DocumentManager::replaceUrlWithNewCourseCode(
- $answer['answer'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ }
+ $em->flush();
+ }
- $answer['comment'] = DocumentManager::replaceUrlWithNewCourseCode(
- $answer['comment'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ if (defined('MULTIPLE_ANSWER_TRUE_FALSE') && MULTIPLE_ANSWER_TRUE_FALSE === $qType) {
+ $newOptByOld = [];
+ if (isset($question->question_options) && is_iterable($question->question_options)) {
+ foreach ($question->question_options as $optWrap) {
+ $opt = $optWrap->obj ?? $optWrap;
+ $optEntity = (new CQuizQuestionOption())
+ ->setQuestion($entity)
+ ->setTitle((string)$opt->name)
+ ->setPosition((int)$opt->position);
+ $em->persist($optEntity);
+ $em->flush();
+ $newOptByOld[$opt->id] = (int)$optEntity->getIid();
+ }
+ foreach ($entity->getAnswers() as $dstAns) {
+ $corr = $dstAns->getCorrect();
+ if ($corr !== null && isset($newOptByOld[$corr])) {
+ $dstAns->setCorrect((int)$newOptByOld[$corr]);
+ $em->persist($dstAns);
+ }
+ }
+ $em->flush();
+ }
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'question_id' => $new_id,
- 'answer' => self::DBUTF8($answer['answer']),
- 'correct' => $answer['correct'],
- 'comment' => false === $answer['comment'] ? '' : self::DBUTF8($answer['comment']),
- 'ponderation' => $answer['ponderation'],
- 'position' => $answer['position'],
- 'hotspot_coordinates' => $answer['hotspot_coordinates'],
- 'hotspot_type' => $answer['hotspot_type'],
- 'id_auto' => 0,
- 'destination' => '',
- ];
+ $this->course->resources[RESOURCE_QUIZQUESTION][$id]->destination_id = $new_id;
- $answerId = Database::insert($table_ans, $params);
+ return $new_id;
+ }
- if ($answerId) {
- $sql = "UPDATE $table_ans SET id = iid, id_auto = iid WHERE iid = $answerId";
- Database::query($sql);
- }
+ public function restore_surveys($sessionId = 0)
+ {
+ if (!$this->course->has_resources(RESOURCE_SURVEY)) {
+ $this->debug && error_log('COURSE_DEBUG: restore_surveys: no survey resources in backup.');
+ return;
+ }
- $correctAnswers[$answerId] = $answer['correct'];
- $onlyAnswers[$answerId] = $answer['answer'];
- }
- }
+ $em = Database::getManager();
+ $surveyRepo = Container::getSurveyRepository();
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ $sessionEntity = $sessionId ? api_get_session_entity((int)$sessionId) : null;
- // Current course id
- $course_id = api_get_course_int_id();
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+ if ($backupRoot === '') {
+ $this->debug && error_log('COURSE_DEBUG: restore_surveys: backupRoot empty; URL rewriting may be partial.');
+ }
- // Moving quiz_question_options
- if (MULTIPLE_ANSWER_TRUE_FALSE == $question->quiz_type) {
- $question_option_list = Question::readQuestionOption($id, $course_id);
+ $resources = $this->course->resources;
- // Question copied from the current platform
- if ($question_option_list) {
- $old_option_ids = [];
- foreach ($question_option_list as $item) {
- $old_id = $item['iid'];
- unset($item['iid']);
- if (isset($item['iid'])) {
- unset($item['iid']);
- }
- $item['question_id'] = $new_id;
- $item['c_id'] = $this->destination_course_id;
- $question_option_id = Database::insert($table_options, $item);
- if ($question_option_id) {
- $old_option_ids[$old_id] = $question_option_id;
- $sql = "UPDATE $table_options SET id = iid WHERE iid = $question_option_id";
- Database::query($sql);
- }
- }
- if ($old_option_ids) {
- $new_answers = Database::select(
- 'iid, correct',
- $table_ans,
- [
- 'WHERE' => [
- 'question_id = ? AND c_id = ? ' => [
- $new_id,
- $this->destination_course_id,
- ],
- ],
- ]
- );
+ foreach ($resources[RESOURCE_SURVEY] as $legacySurveyId => $surveyObj) {
+ try {
+ $code = (string)($surveyObj->code ?? '');
+ $lang = (string)($surveyObj->lang ?? '');
- foreach ($new_answers as $answer_item) {
- $params = [];
- $params['correct'] = $old_option_ids[$answer_item['correct']];
- Database::update(
- $table_ans,
- $params,
- [
- 'iid = ? AND c_id = ? AND question_id = ? ' => [
- $answer_item['iid'],
- $this->destination_course_id,
- $new_id,
- ],
- ],
- false
- );
- }
+ $title = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($surveyObj->title ?? ''), $courseEntity, $backupRoot) ?? (string)($surveyObj->title ?? '');
+ $subtitle = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($surveyObj->subtitle ?? ''), $courseEntity, $backupRoot) ?? (string)($surveyObj->subtitle ?? '');
+ $intro = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($surveyObj->intro ?? ''), $courseEntity, $backupRoot) ?? (string)($surveyObj->intro ?? '');
+ $surveyThanks = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($surveyObj->surveythanks ?? ''), $courseEntity, $backupRoot) ?? (string)($surveyObj->surveythanks ?? '');
+
+ $onePerPage = !empty($surveyObj->one_question_per_page);
+ $shuffle = isset($surveyObj->shuffle) ? (bool)$surveyObj->shuffle : (!empty($surveyObj->suffle));
+ $anonymous = (string)((int)($surveyObj->anonymous ?? 0));
+
+ try { $creationDate = !empty($surveyObj->creation_date) ? new \DateTime((string)$surveyObj->creation_date) : new \DateTime(); } catch (\Throwable) { $creationDate = new \DateTime(); }
+ try { $availFrom = !empty($surveyObj->avail_from) ? new \DateTime((string)$surveyObj->avail_from) : null; } catch (\Throwable) { $availFrom = null; }
+ try { $availTill = !empty($surveyObj->avail_till) ? new \DateTime((string)$surveyObj->avail_till) : null; } catch (\Throwable) { $availTill = null; }
+
+ $visibleResults = isset($surveyObj->visible_results) ? (int)$surveyObj->visible_results : null;
+ $displayQuestionNumber = isset($surveyObj->display_question_number) ? (bool)$surveyObj->display_question_number : true;
+
+ $existing = null;
+ try {
+ if (method_exists($surveyRepo, 'findOneByCodeAndLangInCourse')) {
+ $existing = $surveyRepo->findOneByCodeAndLangInCourse($courseEntity, $code, $lang);
+ } else {
+ $existing = $surveyRepo->findOneBy(['code' => $code, 'lang' => $lang]);
}
- } else {
- $new_options = [];
- if (isset($question->question_options)) {
- foreach ($question->question_options as $obj) {
- $item = [];
- $item['question_id'] = $new_id;
- $item['c_id'] = $this->destination_course_id;
- $item['name'] = $obj->obj->name;
- $item['position'] = $obj->obj->position;
- $question_option_id = Database::insert($table_options, $item);
-
- if ($question_option_id) {
- $new_options[$obj->obj->id] = $question_option_id;
- $sql = "UPDATE $table_options SET id = iid WHERE iid = $question_option_id";
- Database::query($sql);
+ } catch (\Throwable $e) {
+ $this->debug && error_log('COURSE_DEBUG: restore_surveys: duplicate check skipped: '.$e->getMessage());
+ }
+
+ if ($existing instanceof CSurvey) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $this->course->resources[RESOURCE_SURVEY][$legacySurveyId]->destination_id = (int)$existing->getIid();
+ $this->debug && error_log("COURSE_DEBUG: restore_surveys: survey exists code='$code' (skip).");
+ continue 2;
+
+ case FILE_RENAME:
+ $base = $code.'_';
+ $i = 1;
+ $try = $base.$i;
+ while (!$this->is_survey_code_available($try)) {
+ $try = $base.(++$i);
}
- }
+ $code = $try;
+ $this->debug && error_log("COURSE_DEBUG: restore_surveys: renaming to '$code'.");
+ break;
- foreach ($correctAnswers as $answer_id => $correct_answer) {
- $params = [];
- $params['correct'] = isset($new_options[$correct_answer]) ? $new_options[$correct_answer] : '';
- Database::update(
- $table_ans,
- $params,
- [
- 'id = ? AND c_id = ? AND question_id = ? ' => [
- $answer_id,
- $this->destination_course_id,
- $new_id,
- ],
- ],
- false
- );
- }
+ case FILE_OVERWRITE:
+ \SurveyManager::deleteSurvey($existing);
+ $em->flush();
+ $this->debug && error_log("COURSE_DEBUG: restore_surveys: existing survey deleted (overwrite).");
+ break;
+
+ default:
+ $this->course->resources[RESOURCE_SURVEY][$legacySurveyId]->destination_id = (int)$existing->getIid();
+ continue 2;
}
}
- }
- // Fix correct answers
- if (in_array($question->quiz_type, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE])) {
- foreach ($correctAnswers as $answer_id => $correct_answer) {
- $params = [];
+ // --- Create survey ---
+ $newSurvey = new CSurvey();
+ $newSurvey
+ ->setCode($code)
+ ->setTitle($title)
+ ->setSubtitle($subtitle)
+ ->setLang($lang)
+ ->setAvailFrom($availFrom)
+ ->setAvailTill($availTill)
+ ->setIsShared((string)($surveyObj->is_shared ?? '0'))
+ ->setTemplate((string)($surveyObj->template ?? 'template'))
+ ->setIntro($intro)
+ ->setSurveythanks($surveyThanks)
+ ->setCreationDate($creationDate)
+ ->setInvited(0)
+ ->setAnswered(0)
+ ->setInviteMail((string)($surveyObj->invite_mail ?? ''))
+ ->setReminderMail((string)($surveyObj->reminder_mail ?? ''))
+ ->setOneQuestionPerPage($onePerPage)
+ ->setShuffle($shuffle)
+ ->setAnonymous($anonymous)
+ ->setDisplayQuestionNumber($displayQuestionNumber);
+
+ if (method_exists($newSurvey, 'setParent')) {
+ $newSurvey->setParent($courseEntity);
+ }
+ $newSurvey->addCourseLink($courseEntity, $sessionEntity);
+
+ if (method_exists($surveyRepo, 'create')) {
+ $surveyRepo->create($newSurvey);
+ } else {
+ $em->persist($newSurvey);
+ $em->flush();
+ }
- if (isset($allAnswers[$correct_answer])) {
- $correct = '';
- foreach ($onlyAnswers as $key => $value) {
- if ($value == $allAnswers[$correct_answer]) {
- $correct = $key;
+ $newId = (int)$newSurvey->getIid();
+ $this->course->resources[RESOURCE_SURVEY][$legacySurveyId]->destination_id = $newId;
- break;
- }
+ // --- Restore questions ---
+ $questionIds = is_array($surveyObj->question_ids ?? null) ? $surveyObj->question_ids : [];
+ if (empty($questionIds) && !empty($resources[RESOURCE_SURVEYQUESTION])) {
+ foreach ($resources[RESOURCE_SURVEYQUESTION] as $qid => $qWrap) {
+ $q = (isset($qWrap->obj) && is_object($qWrap->obj)) ? $qWrap->obj : $qWrap;
+ if ((int)($q->survey_id ?? 0) === (int)$legacySurveyId) {
+ $questionIds[] = (int)$qid;
}
-
- $params['correct'] = $correct;
- Database::update(
- $table_ans,
- $params,
- [
- 'id = ? AND c_id = ? AND question_id = ? ' => [
- $answer_id,
- $this->destination_course_id,
- $new_id,
- ],
- ]
- );
}
}
- }
- $this->course->resources[RESOURCE_QUIZQUESTION][$id]->destination_id = $new_id;
- }
+ foreach ($questionIds as $legacyQid) {
+ $this->restore_survey_question((int)$legacyQid, $newId);
+ }
- return $new_id;
+ $this->debug && error_log("COURSE_DEBUG: restore_surveys: created survey iid={$newId}, questions=".count($questionIds));
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_surveys: failed: '.$e->getMessage());
+ }
+ }
}
+
/**
- * @todo : add session id when used for session
+ * Restore survey-questions (legacy signature). $survey_id is the NEW iid.
*/
- public function restore_test_category($session_id, $respect_base_content, $destination_course_code)
+ public function restore_survey_question($id, $survey_id)
{
- if (!empty($session_id)) {
- return false;
+ $resources = $this->course->resources;
+ $qWrap = $resources[RESOURCE_SURVEYQUESTION][$id] ?? null;
+
+ if (!$qWrap || !is_object($qWrap)) {
+ $this->debug && error_log("COURSE_DEBUG: restore_survey_question: legacy question $id not found.");
+ return 0;
+ }
+ if (method_exists($qWrap, 'is_restored') && $qWrap->is_restored()) {
+ return $qWrap->destination_id;
}
- $destinationCourseId = $this->destination_course_info['real_id'];
- // Let's restore the categories
- $categoryOldVsNewList = []; // used to build the quiz_question_rel_category table
- if ($this->course->has_resources(RESOURCE_TEST_CATEGORY)) {
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_TEST_CATEGORY] as $id => $courseCopyTestCategory) {
- $categoryOldVsNewList[$courseCopyTestCategory->source_id] = $id;
- // check if this test_category already exist in the destination BDD
- // do not Database::escape_string $title and $description, it will be done later
- $title = $courseCopyTestCategory->title;
- $description = $courseCopyTestCategory->description;
- if (TestCategory::categoryTitleExists($title, $destinationCourseId)) {
- switch ($this->file_option) {
- case FILE_SKIP:
- //Do nothing
- break;
- case FILE_RENAME:
- $new_title = $title.'_';
- while (TestCategory::categoryTitleExists($new_title, $destinationCourseId)) {
- $new_title .= '_';
- }
- $test_category = new TestCategory();
- $test_category->name = $new_title;
- $test_category->description = $description;
- $new_id = $test_category->save($destinationCourseId);
- $categoryOldVsNewList[$courseCopyTestCategory->source_id] = $new_id;
- break;
- case FILE_OVERWRITE:
- // get category from source
- $destinationCategoryId = TestCategory::get_category_id_for_title(
- $title,
- $destinationCourseId
- );
- if ($destinationCategoryId) {
- $my_cat = new TestCategory();
- $my_cat = $my_cat->getCategory($destinationCategoryId, $destinationCourseId);
- $my_cat->name = $title;
- $my_cat->description = $description;
- $my_cat->modifyCategory($destinationCourseId);
- $categoryOldVsNewList[$courseCopyTestCategory->source_id] = $destinationCategoryId;
- }
+ $surveyRepo = Container::getSurveyRepository();
+ $em = Database::getManager();
+ $courseEntity = api_get_course_entity($this->destination_course_id);
- break;
- }
- } else {
- // create a new test_category
- $test_category = new TestCategory();
- $test_category->name = $title;
- $test_category->description = $description;
- $new_id = $test_category->save($destinationCourseId);
- $categoryOldVsNewList[$courseCopyTestCategory->source_id] = $new_id;
- }
- $this->course->resources[RESOURCE_TEST_CATEGORY][$id]->destination_id = $categoryOldVsNewList[$courseCopyTestCategory->source_id];
- }
- }
-
- // lets check if quizzes-question are restored too,
- // to redo the link between test_category and quizzes question for questions restored
- // we can use the source_id field
- // question source_id => category source_id
- if ($this->course->has_resources(RESOURCE_QUIZQUESTION)) {
- // check the category number of each question restored
- if (!empty($resources[RESOURCE_QUIZQUESTION])) {
- foreach ($resources[RESOURCE_QUIZQUESTION] as $id => $courseCopyQuestion) {
- $newQuestionId = $resources[RESOURCE_QUIZQUESTION][$id]->destination_id;
- $questionCategoryId = $courseCopyQuestion->question_category;
- if ($newQuestionId > 0 &&
- $questionCategoryId > 0 &&
- isset($categoryOldVsNewList[$questionCategoryId])
- ) {
- TestCategory::addCategoryToQuestion(
- $categoryOldVsNewList[$questionCategoryId],
- $newQuestionId,
- $destinationCourseId
- );
- }
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+
+ $survey = $surveyRepo->find((int)$survey_id);
+ if (!$survey instanceof CSurvey) {
+ $this->debug && error_log("COURSE_DEBUG: restore_survey_question: target survey $survey_id not found.");
+ return 0;
+ }
+
+ $q = (isset($qWrap->obj) && is_object($qWrap->obj)) ? $qWrap->obj : $qWrap;
+
+ // Rewrite HTML
+ $questionText = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($q->survey_question ?? ''), $courseEntity, $backupRoot) ?? (string)($q->survey_question ?? '');
+ $commentText = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($q->survey_question_comment ?? ''), $courseEntity, $backupRoot) ?? (string)($q->survey_question_comment ?? '');
+
+ try {
+ $question = new CSurveyQuestion();
+ $question
+ ->setSurvey($survey)
+ ->setSurveyQuestion($questionText)
+ ->setSurveyQuestionComment($commentText)
+ ->setType((string)($q->survey_question_type ?? $q->type ?? 'open'))
+ ->setDisplay((string)($q->display ?? 'vertical'))
+ ->setSort((int)($q->sort ?? 0));
+
+ if (isset($q->shared_question_id) && method_exists($question, 'setSharedQuestionId')) {
+ $question->setSharedQuestionId((int)$q->shared_question_id);
+ }
+ if (isset($q->max_value) && method_exists($question, 'setMaxValue')) {
+ $question->setMaxValue((int)$q->max_value);
+ }
+ if (isset($q->is_required)) {
+ if (method_exists($question, 'setIsMandatory')) {
+ $question->setIsMandatory((bool)$q->is_required);
+ } elseif (method_exists($question, 'setIsRequired')) {
+ $question->setIsRequired((bool)$q->is_required);
}
}
+
+ $em->persist($question);
+ $em->flush();
+
+ // Options (value NOT NULL: default to 0 if missing)
+ $answers = is_array($q->answers ?? null) ? $q->answers : [];
+ foreach ($answers as $idx => $answer) {
+ $optText = ChamiloHelper::rewriteLegacyCourseUrlsToAssets((string)($answer['option_text'] ?? ''), $courseEntity, $backupRoot) ?? (string)($answer['option_text'] ?? '');
+ $value = isset($answer['value']) && $answer['value'] !== null ? (int)$answer['value'] : 0;
+ $sort = (int)($answer['sort'] ?? ($idx + 1));
+
+ $opt = new CSurveyQuestionOption();
+ $opt
+ ->setSurvey($survey)
+ ->setQuestion($question)
+ ->setOptionText($optText)
+ ->setSort($sort)
+ ->setValue($value);
+
+ $em->persist($opt);
+ }
+ $em->flush();
+
+ $this->course->resources[RESOURCE_SURVEYQUESTION][$id]->destination_id = (int)$question->getIid();
+
+ return (int)$question->getIid();
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_survey_question: failed: '.$e->getMessage());
+ return 0;
+ }
+ }
+
+
+ public function is_survey_code_available($survey_code)
+ {
+ $survey_code = (string)$survey_code;
+ $surveyRepo = Container::getSurveyRepository();
+
+ try {
+ $hit = $surveyRepo->findOneBy(['code' => $survey_code]);
+ return $hit ? false : true;
+ } catch (\Throwable $e) {
+ $this->debug && error_log('COURSE_DEBUG: is_survey_code_available: fallback failed: '.$e->getMessage());
+ return true;
}
}
/**
- * Restore surveys.
- *
- * @param int $sessionId Optional. The session id
+ * @param int $sessionId
+ * @param bool $baseContent
*/
- public function restore_surveys($sessionId = 0)
+ public function restore_learnpath_category(int $sessionId = 0, bool $baseContent = false): void
{
- $sessionId = (int) $sessionId;
- if ($this->course->has_resources(RESOURCE_SURVEY)) {
- $table_sur = Database::get_course_table(TABLE_SURVEY);
- $table_que = Database::get_course_table(TABLE_SURVEY_QUESTION);
- $table_ans = Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_SURVEY] as $id => $survey) {
- $sql = 'SELECT survey_id FROM '.$table_sur.'
- WHERE
- c_id = '.$this->destination_course_id.' AND
- code = "'.self::DBUTF8escapestring($survey->code).'" AND
- lang = "'.self::DBUTF8escapestring($survey->lang).'" ';
-
- $result_check = Database::query($sql);
-
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $survey->title = DocumentManager::replaceUrlWithNewCourseCode(
- $survey->title,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
-
- $survey->subtitle = DocumentManager::replaceUrlWithNewCourseCode(
- $survey->subtitle,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ $reuseExisting = false;
+ if (isset($this->tool_copy_settings['learnpath_category']['reuse_existing']) &&
+ true === $this->tool_copy_settings['learnpath_category']['reuse_existing']) {
+ $reuseExisting = true;
+ }
- $survey->intro = DocumentManager::replaceUrlWithNewCourseCode(
- $survey->intro,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ if (!$this->course->has_resources(RESOURCE_LEARNPATH_CATEGORY)) {
+ return;
+ }
- $survey->surveythanks = DocumentManager::replaceUrlWithNewCourseCode(
- $survey->surveythanks,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ $tblLpCategory = Database::get_course_table(TABLE_LP_CATEGORY);
+ $resources = $this->course->resources;
- $params = [
- 'c_id' => $this->destination_course_id,
- 'code' => self::DBUTF8($survey->code),
- 'title' => false === $survey->title ? '' : self::DBUTF8($survey->title),
- 'subtitle' => false === $survey->subtitle ? '' : self::DBUTF8($survey->subtitle),
- 'author' => self::DBUTF8($survey->author),
- 'lang' => self::DBUTF8($survey->lang),
- 'avail_from' => self::DBUTF8($survey->avail_from),
- 'avail_till' => self::DBUTF8($survey->avail_till),
- 'is_shared' => self::DBUTF8($survey->is_shared),
- 'template' => self::DBUTF8($survey->template),
- 'intro' => false === $survey->intro ? '' : self::DBUTF8($survey->intro),
- 'surveythanks' => false === $survey->surveythanks ? '' : self::DBUTF8($survey->surveythanks),
- 'creation_date' => self::DBUTF8($survey->creation_date),
- 'invited' => '0',
- 'answered' => '0',
- 'invite_mail' => self::DBUTF8($survey->invite_mail),
- 'reminder_mail' => self::DBUTF8($survey->reminder_mail),
- 'session_id' => $sessionId,
- 'one_question_per_page' => isset($survey->one_question_per_page) ? $survey->one_question_per_page : 0,
- 'shuffle' => isset($survey->suffle) ? $survey->suffle : 0,
- ];
+ /** @var LearnPathCategory $item */
+ foreach ($resources[RESOURCE_LEARNPATH_CATEGORY] as $id => $item) {
+ /** @var CLpCategory|null $lpCategory */
+ $lpCategory = $item->object;
- // An existing survey exists with the same code and the same language
- if (1 == Database::num_rows($result_check)) {
- switch ($this->file_option) {
- case FILE_SKIP:
- //Do nothing
- break;
- case FILE_RENAME:
- $survey_code = $survey->code.'_';
- $i = 1;
- $temp_survey_code = $survey_code.$i;
- while (!$this->is_survey_code_available($temp_survey_code)) {
- $temp_survey_code = $survey_code.++$i;
- }
- $survey_code = $temp_survey_code;
-
- $params['code'] = $survey_code;
- $new_id = Database::insert($table_sur, $params);
- if ($new_id) {
- $sql = "UPDATE $table_sur SET survey_id = iid WHERE iid = $new_id";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_SURVEY][$id]->destination_id = $new_id;
- foreach ($survey->question_ids as $index => $question_id) {
- $qid = $this->restore_survey_question($question_id, $new_id);
- $sql = "UPDATE $table_que SET survey_id = $new_id
- WHERE c_id = ".$this->destination_course_id." AND question_id = $qid";
- Database::query($sql);
- $sql = "UPDATE $table_ans SET survey_id = $new_id
- WHERE c_id = ".$this->destination_course_id." AND question_id = $qid";
- Database::query($sql);
- }
- }
+ if (!$lpCategory) {
+ continue;
+ }
- break;
- case FILE_OVERWRITE:
- // Delete the existing survey with the same code and language and
- // import the one of the source course
- // getting the information of the survey (used for when the survey is shared)
- $sql = "SELECT * FROM $table_sur
- WHERE
- c_id = ".$this->destination_course_id." AND
- survey_id='".self::DBUTF8escapestring(Database::result($result_check, 0, 0))."'";
- $result = Database::query($sql);
- $survey_data = Database::fetch_assoc($result);
-
- // if the survey is shared => also delete the shared content
- if (isset($survey_data['survey_share']) && is_numeric($survey_data['survey_share'])) {
- SurveyManager::delete_survey(
- $survey_data['survey_share'],
- true,
- $this->destination_course_id
- );
- }
- SurveyManager::delete_survey(
- $survey_data['survey_id'],
- false,
- $this->destination_course_id
- );
+ $title = trim($lpCategory->getTitle());
+ if ($title === '') {
+ continue;
+ }
- // Insert the new source survey
- $new_id = Database::insert($table_sur, $params);
+ $categoryId = 0;
+
+ $existing = Database::select(
+ 'iid',
+ $tblLpCategory,
+ [
+ 'WHERE' => [
+ 'c_id = ? AND name = ?' => [$this->destination_course_id, $title],
+ ],
+ ],
+ 'first'
+ );
- if ($new_id) {
- $sql = "UPDATE $table_sur SET survey_id = iid WHERE iid = $new_id";
- Database::query($sql);
+ if ($reuseExisting && !empty($existing) && !empty($existing['iid'])) {
+ $categoryId = (int) $existing['iid'];
+ } else {
+ $values = [
+ 'c_id' => $this->destination_course_id,
+ 'name' => $title,
+ ];
- $this->course->resources[RESOURCE_SURVEY][$id]->destination_id = $new_id;
- foreach ($survey->question_ids as $index => $question_id) {
- $qid = $this->restore_survey_question(
- $question_id,
- $new_id
- );
- $sql = "UPDATE $table_que SET survey_id = $new_id
- WHERE c_id = ".$this->destination_course_id." AND question_id = $qid";
- Database::query($sql);
- $sql = "UPDATE $table_ans SET survey_id = $new_id
- WHERE c_id = ".$this->destination_course_id." AND question_id = $qid";
- Database::query($sql);
- }
- }
+ $categoryId = (int) learnpath::createCategory($values);
+ }
- break;
- default:
- break;
- }
- } else {
- // No existing survey with the same language and the same code, we just copy the survey
- $new_id = Database::insert($table_sur, $params);
-
- if ($new_id) {
- $sql = "UPDATE $table_sur SET survey_id = iid WHERE iid = $new_id";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_SURVEY][$id]->destination_id = $new_id;
- foreach ($survey->question_ids as $index => $question_id) {
- $qid = $this->restore_survey_question(
- $question_id,
- $new_id
- );
- $sql = "UPDATE $table_que SET survey_id = $new_id
- WHERE c_id = ".$this->destination_course_id." AND question_id = $qid";
- Database::query($sql);
- $sql = "UPDATE $table_ans SET survey_id = $new_id
- WHERE c_id = ".$this->destination_course_id." AND question_id = $qid";
- Database::query($sql);
- }
- }
- }
+ if ($categoryId > 0) {
+ $this->course->resources[RESOURCE_LEARNPATH_CATEGORY][$id]->destination_id = $categoryId;
}
}
}
/**
- * Check availability of a survey code.
- *
- * @param string $survey_code
+ * Zip a SCORM folder (must contain imsmanifest.xml) into a temp ZIP.
+ * Returns absolute path to the temp ZIP or null on error.
*/
- public function is_survey_code_available($survey_code): bool
+ private function zipScormFolder(string $folderAbs): ?string
{
- $table_sur = Database::get_course_table(TABLE_SURVEY);
- $sql = "SELECT * FROM $table_sur
- WHERE
- c_id = ".$this->destination_course_id." AND
- code = '".self::DBUTF8escapestring($survey_code)."'";
- $result = Database::query($sql);
- if (Database::num_rows($result) > 0) {
- return false;
- } else {
- return true;
+ $folderAbs = rtrim($folderAbs, '/');
+ $manifest = $folderAbs.'/imsmanifest.xml';
+ if (!is_file($manifest)) {
+ error_log("SCORM ZIPPER: 'imsmanifest.xml' not found in folder: $folderAbs");
+ return null;
+ }
+
+ $tmpZip = sys_get_temp_dir().'/scorm_'.uniqid('', true).'.zip';
+
+ try {
+ $zip = new ZipFile();
+ // Put folder contents at the ZIP root – important for SCORM imports
+ $zip->addDirRecursive($folderAbs, '');
+ $zip->saveAsFile($tmpZip);
+ $zip->close();
+ } catch (\Throwable $e) {
+ error_log("SCORM ZIPPER: Failed to create temp zip: ".$e->getMessage());
+ return null;
+ }
+
+ if (!is_file($tmpZip) || filesize($tmpZip) === 0) {
+ @unlink($tmpZip);
+ error_log("SCORM ZIPPER: Temp zip is empty or missing: $tmpZip");
+ return null;
}
+
+ return $tmpZip;
}
/**
- * Restore survey-questions.
+ * Find a SCORM package for a given LP.
+ * It returns ['zip' => , 'temp' => true if zip is temporary].
*
- * @param int $id
- * @param string $survey_id
+ * Search order:
+ * 1) resources[SCORM] entries bound to this LP (zip or path).
+ * - If 'path' is a folder containing imsmanifest.xml, it will be zipped on the fly.
+ * 2) Heuristics: scan typical folders for *.zip
+ * 3) Heuristics: scan backup recursively for an imsmanifest.xml, then zip that folder.
*/
- public function restore_survey_question($id, $survey_id)
+ private function findScormPackageForLp(int $srcLpId): array
{
- $resources = $this->course->resources;
- $question = $resources[RESOURCE_SURVEYQUESTION][$id];
- $new_id = 0;
+ $out = ['zip' => null, 'temp' => false];
+ $base = rtrim($this->course->backup_path, '/');
+
+ // 1) Direct mapping from SCORM bucket
+ if (!empty($this->course->resources[RESOURCE_SCORM]) && is_array($this->course->resources[RESOURCE_SCORM])) {
+ foreach ($this->course->resources[RESOURCE_SCORM] as $sc) {
+ $src = isset($sc->source_lp_id) ? (int) $sc->source_lp_id : 0;
+ $dst = isset($sc->lp_id_dest) ? (int) $sc->lp_id_dest : 0;
+ $match = ($src && $src === $srcLpId);
+
+ if (
+ !$match &&
+ $dst &&
+ !empty($this->course->resources[RESOURCE_LEARNPATH][$srcLpId]->destination_id)
+ ) {
+ $match = ($dst === (int) $this->course->resources[RESOURCE_LEARNPATH][$srcLpId]->destination_id);
+ }
+ if (!$match) { continue; }
- if (is_object($question)) {
- if ($question->is_restored()) {
- return $question->destination_id;
- }
- $table_que = Database::get_course_table(TABLE_SURVEY_QUESTION);
- $table_ans = Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION);
+ $cands = [];
+ if (!empty($sc->zip)) { $cands[] = $base.'/'.ltrim((string) $sc->zip, '/'); }
+ if (!empty($sc->path)) { $cands[] = $base.'/'.ltrim((string) $sc->path, '/'); }
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $question->survey_question = DocumentManager::replaceUrlWithNewCourseCode(
- $question->survey_question,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ foreach ($cands as $abs) {
+ if (is_file($abs) && is_readable($abs)) {
+ $out['zip'] = $abs;
+ $out['temp'] = false;
+ return $out;
+ }
+ if (is_dir($abs) && is_readable($abs)) {
+ $tmp = $this->zipScormFolder($abs);
+ if ($tmp) {
+ $out['zip'] = $tmp;
+ $out['temp'] = true;
+ return $out;
+ }
+ }
+ }
+ }
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'survey_id' => self::DBUTF8($survey_id),
- 'survey_question' => false === $question->survey_question ? '' : self::DBUTF8($question->survey_question),
- 'survey_question_comment' => self::DBUTF8($question->survey_question_comment),
- 'type' => self::DBUTF8($question->survey_question_type),
- 'display' => self::DBUTF8($question->display),
- 'sort' => self::DBUTF8($question->sort),
- 'shared_question_id' => self::DBUTF8($question->shared_question_id),
- 'max_value' => self::DBUTF8($question->max_value),
- ];
- if (isset($question->is_required)) {
- $params['is_required'] = $question->is_required;
- }
-
- $new_id = Database::insert($table_que, $params);
- if ($new_id) {
- $sql = "UPDATE $table_que SET question_id = iid WHERE iid = $new_id";
- Database::query($sql);
-
- foreach ($question->answers as $index => $answer) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $answer['option_text'] = DocumentManager::replaceUrlWithNewCourseCode(
- $answer['option_text'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ // 2) Heuristic: typical folders with *.zip
+ foreach (['/scorm','/document/scorm','/documents/scorm'] as $dir) {
+ $full = $base.$dir;
+ if (!is_dir($full)) { continue; }
+ $glob = glob($full.'/*.zip') ?: [];
+ if (!empty($glob)) {
+ $out['zip'] = $glob[0];
+ $out['temp'] = false;
+ return $out;
+ }
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'question_id' => $new_id,
- 'option_text' => false === $answer['option_text'] ? '' : self::DBUTF8($answer['option_text']),
- 'sort' => $answer['sort'],
- 'survey_id' => self::DBUTF8($survey_id),
- ];
- $answerId = Database::insert($table_ans, $params);
- if ($answerId) {
- $sql = "UPDATE $table_ans SET question_option_id = iid
- WHERE iid = $answerId";
- Database::query($sql);
+ // 3) Heuristic: look for imsmanifest.xml anywhere, then zip that folder
+ $riiFlags = \FilesystemIterator::SKIP_DOTS;
+ try {
+ $rii = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($base, $riiFlags),
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+ foreach ($rii as $f) {
+ if ($f->isFile() && strtolower($f->getFilename()) === 'imsmanifest.xml') {
+ $folder = $f->getPath();
+ $tmp = $this->zipScormFolder($folder);
+ if ($tmp) {
+ $out['zip'] = $tmp;
+ $out['temp'] = true;
+ return $out;
}
}
- $this->course->resources[RESOURCE_SURVEYQUESTION][$id]->destination_id = $new_id;
}
+ } catch (\Throwable $e) {
+ error_log("SCORM FINDER: Recursive scan failed: ".$e->getMessage());
}
- return $new_id;
+ return $out;
}
/**
- * @param int $sessionId
- * @param bool $baseContent
+ * Restore SCORM ZIPs under Documents (Learning paths) for traceability.
+ * Accepts real zips and on-the-fly temporary ones (temp will be deleted after upload).
*/
- public function restore_learnpath_category($sessionId = 0, $baseContent = false)
+ public function restore_scorm_documents(): void
{
- $reuseExisting = false;
+ $logp = 'RESTORE_SCORM_ZIP: ';
- if (isset($this->tool_copy_settings['learnpath_category']) &&
- isset($this->tool_copy_settings['learnpath_category']['reuse_existing']) &&
- true === $this->tool_copy_settings['learnpath_category']['reuse_existing']
- ) {
- $reuseExisting = true;
- }
+ $getBucket = function(string $type) {
+ if (!empty($this->course->resources[$type]) && is_array($this->course->resources[$type])) {
+ return $this->course->resources[$type];
+ }
+ foreach ($this->course->resources ?? [] as $k => $v) {
+ if (is_string($k) && strtolower($k) === strtolower($type) && is_array($v)) {
+ return $v;
+ }
+ }
+ return [];
+ };
- $tblLpCategory = Database::get_course_table(TABLE_LP_CATEGORY);
+ /** @var \Chamilo\CourseBundle\Repository\CDocumentRepository $docRepo */
+ $docRepo = Container::getDocumentRepository();
+ $em = Database::getManager();
- if ($this->course->has_resources(RESOURCE_LEARNPATH_CATEGORY)) {
- $resources = $this->course->resources;
- /** @var LearnPathCategory $item */
- foreach ($resources[RESOURCE_LEARNPATH_CATEGORY] as $id => $item) {
- /** @var CLpCategory $lpCategory */
- $lpCategory = $item->object;
-
- if ($lpCategory) {
- $categoryId = 0;
-
- $existingLpCategory = Database::select(
- 'iid',
- $tblLpCategory,
- [
- 'WHERE' => [
- 'c_id = ? AND name = ?' => [$this->destination_course_id, $lpCategory->getTitle()],
- ],
- ],
- 'first'
- );
+ $courseInfo = $this->destination_course_info;
+ if (empty($courseInfo) || empty($courseInfo['real_id'])) { error_log($logp.'missing courseInfo/real_id'); return; }
- if ($reuseExisting && !empty($existingLpCategory)) {
- $categoryId = $existingLpCategory['iid'];
- } else {
- $values = [
- 'c_id' => $this->destination_course_id,
- 'name' => $lpCategory->getTitle(),
- ];
- $categoryId = learnpath::createCategory($values);
- }
+ $courseEntity = api_get_course_entity((int) $courseInfo['real_id']);
+ if (!$courseEntity) { error_log($logp.'api_get_course_entity failed'); return; }
- if ($categoryId) {
- $this->course->resources[RESOURCE_LEARNPATH_CATEGORY][$id]->destination_id = $categoryId;
- }
+ $sid = property_exists($this, 'current_session_id') ? (int) $this->current_session_id : 0;
+ $session = api_get_session_entity($sid);
+
+ $entries = [];
+
+ // A) direct SCORM bucket
+ $scormBucket = $getBucket(RESOURCE_SCORM);
+ foreach ($scormBucket as $sc) { $entries[] = $sc; }
+
+ // B) also try LPs that are SCORM
+ $lpBucket = $getBucket(RESOURCE_LEARNPATH);
+ foreach ($lpBucket as $srcLpId => $lpObj) {
+ $lpType = (int)($lpObj->lp_type ?? $lpObj->type ?? 1);
+ if ($lpType === CLp::SCORM_TYPE) {
+ $entries[] = (object)[
+ 'source_lp_id' => (int)$srcLpId,
+ 'lp_id_dest' => (int)($lpObj->destination_id ?? 0),
+ ];
+ }
+ }
+
+ error_log($logp.'entries='.count($entries));
+ if (empty($entries)) { return; }
+
+ $lpTop = $docRepo->ensureLearningPathSystemFolder($courseEntity, $session);
+
+ foreach ($entries as $sc) {
+ // Locate package (zip or folder → temp zip)
+ $srcLpId = (int)($sc->source_lp_id ?? 0);
+ $pkg = $this->findScormPackageForLp($srcLpId);
+ if (empty($pkg['zip'])) {
+ error_log($logp.'No package (zip/folder) found for a SCORM entry');
+ continue;
+ }
+ $zipAbs = $pkg['zip'];
+ $zipTemp = (bool)$pkg['temp'];
+
+ // Map LP title/dest for folder name
+ $lpId = 0; $lpTitle = 'Untitled';
+ if (!empty($sc->lp_id_dest)) {
+ $lpId = (int) $sc->lp_id_dest;
+ } elseif ($srcLpId && !empty($lpBucket[$srcLpId]->destination_id)) {
+ $lpId = (int) $lpBucket[$srcLpId]->destination_id;
+ }
+ $lpEntity = $lpId ? Container::getLpRepository()->find($lpId) : null;
+ if ($lpEntity) { $lpTitle = $lpEntity->getTitle() ?: $lpTitle; }
+
+ $cleanTitle = preg_replace('/\s+/', ' ', trim(str_replace(['/', '\\'], '-', (string)$lpTitle))) ?: 'Untitled';
+ $folderTitleBase = sprintf('SCORM - %d - %s', $lpId ?: 0, $cleanTitle);
+ $folderTitle = $folderTitleBase;
+
+ $exists = $docRepo->findChildNodeByTitle($lpTop, $folderTitle);
+ if ($exists) {
+ if ($this->file_option === FILE_SKIP) {
+ error_log($logp."Skip due to folder name collision: '$folderTitle'");
+ if ($zipTemp) { @unlink($zipAbs); }
+ continue;
+ }
+ if ($this->file_option === FILE_RENAME) {
+ $i = 1;
+ do {
+ $folderTitle = $folderTitleBase.' ('.$i.')';
+ $exists = $docRepo->findChildNodeByTitle($lpTop, $folderTitle);
+ $i++;
+ } while ($exists);
+ }
+ if ($this->file_option === FILE_OVERWRITE && $lpEntity) {
+ $docRepo->purgeScormZip($courseEntity, $lpEntity);
+ $em->flush();
}
}
+
+ // Upload ZIP under Documents
+ $uploaded = new UploadedFile(
+ $zipAbs, basename($zipAbs), 'application/zip', null, true
+ );
+ $lpFolder = $docRepo->ensureFolder(
+ $courseEntity, $lpTop, $folderTitle,
+ ResourceLink::VISIBILITY_DRAFT, $session
+ );
+ $docRepo->createFileInFolder(
+ $courseEntity, $lpFolder, $uploaded,
+ sprintf('SCORM ZIP for LP #%d', $lpId),
+ ResourceLink::VISIBILITY_DRAFT, $session
+ );
+ $em->flush();
+
+ if ($zipTemp) { @unlink($zipAbs); }
+ error_log($logp."ZIP stored under folder '$folderTitle'");
}
}
/**
- * Restoring learning paths.
- *
- * @param int $session_id
- * @param bool|false $respect_base_content
+ * Restore learnpaths (SCORM-aware).
+ * For SCORM LPs, it accepts a real zip or zips a folder-on-the-fly if needed.
+ * This version adds strict checks, robust logging and a guaranteed fallback LP.
*/
- public function restore_learnpaths($session_id = 0, $respect_base_content = false)
+ public function restore_learnpaths($session_id = 0, $respect_base_content = false, $destination_course_code = '')
{
- $session_id = (int) $session_id;
- if ($this->course->has_resources(RESOURCE_LEARNPATH)) {
- $table_main = Database::get_course_table(TABLE_LP_MAIN);
- $table_item = Database::get_course_table(TABLE_LP_ITEM);
- $table_tool = Database::get_course_table(TABLE_TOOL_LIST);
+ $logp = 'RESTORE_LP: ';
- $resources = $this->course->resources;
- $origin_path = $this->course->backup_path.'/upload/learning_path/images/';
- $destination_path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/upload/learning_path/images/';
-
- // Choose default visibility
- $toolVisibility = api_get_setting('tool_visible_by_default_at_creation');
- $defaultLpVisibility = 'invisible';
- if (isset($toolVisibility['learning_path']) && 'true' == $toolVisibility['learning_path']) {
- $defaultLpVisibility = 'visible';
- }
-
- foreach ($resources[RESOURCE_LEARNPATH] as $id => $lp) {
- $condition_session = '';
- if (!empty($session_id)) {
- if ($respect_base_content) {
- $my_session_id = $lp->session_id;
- if (!empty($lp->session_id)) {
- $my_session_id = $session_id;
- }
- $condition_session = $my_session_id;
- } else {
- $session_id = (int) $session_id;
- $condition_session = $session_id;
- }
- }
+ // --- REQUIRED INITIALIZATION (avoid "Undefined variable $courseEntity") ---
+ $courseInfo = $this->destination_course_info ?? [];
+ $courseId = (int)($courseInfo['real_id'] ?? 0);
+ if ($courseId <= 0) {
+ error_log($logp.'Missing destination course id; aborting.');
+ return;
+ }
- // Adding the author's image
- if (!empty($lp->preview_image)) {
- $new_filename = uniqid('').substr(
- $lp->preview_image,
- strlen($lp->preview_image) - 7,
- strlen($lp->preview_image)
- );
+ $courseEntity = api_get_course_entity($courseId);
+ if (!$courseEntity) {
+ error_log($logp.'api_get_course_entity() returned null for id='.$courseId.'; aborting.');
+ return;
+ }
- if (file_exists($origin_path.$lp->preview_image) &&
- !is_dir($origin_path.$lp->preview_image)
- ) {
- $copy_result = copy(
- $origin_path.$lp->preview_image,
- $destination_path.$new_filename
- );
- if ($copy_result) {
- $lp->preview_image = $new_filename;
- // Create 64 version from original
- $temp = new Image($destination_path.$new_filename);
- $temp->resize(64);
- $pathInfo = pathinfo($new_filename);
- if ($pathInfo) {
- $filename = $pathInfo['filename'];
- $extension = $pathInfo['extension'];
- $temp->send_image($destination_path.'/'.$filename.'.64.'.$extension);
- }
- } else {
- $lp->preview_image = '';
- }
- }
- }
+ // Session entity is optional
+ $session = $session_id ? api_get_session_entity((int)$session_id) : null;
+
+ $em = Database::getManager();
+ $lpRepo = Container::getLpRepository();
+
+ /**
+ * Resolve a resource "bucket" by type (constant or string) and return [key, data].
+ * - Normalizes common aliases (case-insensitive).
+ * - Keeps original bucket key so we can write back destination_id on the right slot.
+ */
+ $getBucketWithKey = function (int|string $type) use ($logp) {
+ // Map constants to canonical strings
+ if (is_int($type)) {
+ $type = match ($type) {
+ defined('RESOURCE_LEARNPATH') ? RESOURCE_LEARNPATH : -1 => 'learnpath',
+ defined('RESOURCE_SCORM') ? RESOURCE_SCORM : -2 => 'scorm',
+ default => (string)$type,
+ };
+ }
- if ($this->add_text_in_items) {
- $lp->name .= ' '.get_lang('Copy');
- }
+ // Common legacy aliases
+ $aliases = [
+ 'learnpath' => ['learnpath','coursecopylearnpath','CourseCopyLearnpath','learning_path'],
+ 'scorm' => ['scorm','scormdocument','ScormDocument'],
+ ];
- if (isset($this->tool_copy_settings['learnpaths'])) {
- if (isset($this->tool_copy_settings['learnpaths']['reset_dates']) &&
- $this->tool_copy_settings['learnpaths']['reset_dates']
- ) {
- $lp->created_on = api_get_utc_datetime();
- $lp->modified_on = api_get_utc_datetime();
- $lp->published_on = null;
- }
- }
+ $want = strtolower((string)$type);
+ $wantedKeys = array_unique(array_merge([$type], $aliases[$want] ?? []));
- $lp->expired_on = isset($lp->expired_on) && '0000-00-00 00:00:00' === $lp->expired_on ? null : $lp->expired_on;
- $lp->published_on = isset($lp->published_on) && '0000-00-00 00:00:00' === $lp->published_on ? null : $lp->published_on;
+ $res = is_array($this->course->resources ?? null) ? $this->course->resources : [];
+ if (empty($res)) {
+ error_log($logp."resources array is empty or invalid");
+ return [null, []];
+ }
- if (isset($lp->categoryId)) {
- $lp->categoryId = (int) $lp->categoryId;
+ // 1) Exact match
+ foreach ($wantedKeys as $k) {
+ if (isset($res[$k]) && is_array($res[$k])) {
+ error_log($logp."bucket '". $type ."' found as '$k' (".count($res[$k]).")");
+ return [$k, $res[$k]];
}
-
- $categoryId = 0;
- if (!empty($lp->categoryId)) {
- if (isset($resources[RESOURCE_LEARNPATH_CATEGORY][$lp->categoryId])) {
- $categoryId = $resources[RESOURCE_LEARNPATH_CATEGORY][$lp->categoryId]->destination_id;
- }
+ }
+ // 2) Case-insensitive match
+ $lowerWanted = array_map('strtolower', $wantedKeys);
+ foreach ($res as $k => $v) {
+ if (is_string($k) && in_array(strtolower($k), $lowerWanted, true) && is_array($v)) {
+ error_log($logp."bucket '". $type ."' found as '$k' (".count($v).")");
+ return [$k, $v];
}
- $params = [
- 'c_id' => $this->destination_course_id,
- 'lp_type' => $lp->lp_type,
- 'name' => self::DBUTF8($lp->name),
- 'path' => self::DBUTF8($lp->path),
- 'ref' => $lp->ref,
- 'description' => self::DBUTF8($lp->description),
- 'content_local' => self::DBUTF8($lp->content_local),
- 'default_encoding' => self::DBUTF8($lp->default_encoding),
- 'default_view_mod' => self::DBUTF8($lp->default_view_mod),
- 'prevent_reinit' => self::DBUTF8($lp->prevent_reinit),
- 'force_commit' => self::DBUTF8($lp->force_commit),
- 'content_maker' => self::DBUTF8($lp->content_maker),
- 'display_order' => self::DBUTF8($lp->display_order),
- 'js_lib' => self::DBUTF8($lp->js_lib),
- 'content_license' => self::DBUTF8($lp->content_license),
- 'author' => self::DBUTF8($lp->author),
- //'preview_image' => self::DBUTF8($lp->preview_image),
- 'use_max_score' => self::DBUTF8($lp->use_max_score),
- 'autolaunch' => self::DBUTF8(isset($lp->autolaunch) ? $lp->autolaunch : ''),
- 'created_on' => empty($lp->created_on) ? api_get_utc_datetime() : self::DBUTF8($lp->created_on),
- 'modified_on' => empty($lp->modified_on) ? api_get_utc_datetime() : self::DBUTF8($lp->modified_on),
- 'published_on' => empty($lp->published_on) ? api_get_utc_datetime() : self::DBUTF8($lp->published_on),
- 'expired_on' => self::DBUTF8($lp->expired_on),
- 'debug' => self::DBUTF8($lp->debug),
- 'theme' => '',
- 'session_id' => $session_id,
- 'prerequisite' => 0,
- 'hide_toc_frame' => 0,
- 'seriousgame_mode' => 0,
- 'category_id' => $categoryId,
- 'max_attempts' => 0,
- 'subscribe_users' => 0,
- ];
+ }
- if (!empty($condition_session)) {
- $params['session_id'] = $condition_session;
- }
+ error_log($logp."bucket '".(string)$type."' not found");
+ return [null, []];
+ };
+
+ // Resolve learnpath bucket (returning its actual key to write back destination_id)
+ [$lpBucketKey, $lpBucket] = $getBucketWithKey(defined('RESOURCE_LEARNPATH') ? RESOURCE_LEARNPATH : 'learnpath');
+ if (empty($lpBucket)) {
+ error_log($logp."No LPs to process");
+ return;
+ }
- $new_lp_id = Database::insert($table_main, $params);
+ // Optional: resolve scorm bucket (may be used by other helpers)
+ [$_scormKey, $scormBucket] = $getBucketWithKey(defined('RESOURCE_SCORM') ? RESOURCE_SCORM : 'scorm');
+ error_log($logp."LPs=".count($lpBucket).", SCORM entries=".count($scormBucket));
- if ($new_lp_id) {
- // The following only makes sense if a new LP was
- // created in the destination course
- $sql = "UPDATE $table_main SET id = iid WHERE iid = $new_lp_id";
- Database::query($sql);
+ foreach ($lpBucket as $srcLpId => $lpObj) {
+ $lpName = $lpObj->name ?? ($lpObj->title ?? ('LP '.$srcLpId));
+ $lpType = (int)($lpObj->lp_type ?? $lpObj->type ?? 1); // 2 = SCORM
+ $encoding = $lpObj->default_encoding ?? 'UTF-8';
- if ($lp->visibility) {
- $params = [
- 'c_id' => $this->destination_course_id,
- 'name' => self::DBUTF8($lp->name),
- 'link' => "lp/lp_controller.php?action=view&lp_id=$new_lp_id&sid=$session_id",
- 'image' => 'scormbuilder.gif',
- 'visibility' => '0',
- 'admin' => '0',
- 'address' => 'squaregrey.gif',
- 'session_id' => $session_id,
- ];
- $insertId = Database::insert($table_tool, $params);
- if ($insertId) {
- $sql = "UPDATE $table_tool SET id = iid WHERE iid = $insertId";
- Database::query($sql);
+ error_log($logp."LP src=$srcLpId, name='". $lpName ."', type=".$lpType);
+
+ // ---- SCORM ----
+ if ($lpType === CLp::SCORM_TYPE) {
+ $createdLpId = 0;
+ $zipAbs = null;
+ $zipTemp = false;
+
+ try {
+ // Find a real SCORM ZIP (or zip a folder on-the-fly)
+ $pkg = $this->findScormPackageForLp((int)$srcLpId);
+ $zipAbs = $pkg['zip'] ?? null;
+ $zipTemp = !empty($pkg['temp']);
+
+ if (!$zipAbs || !is_readable($zipAbs)) {
+ error_log($logp."SCORM LP src=$srcLpId: NO ZIP found/readable");
+ } else {
+ error_log($logp."SCORM LP src=$srcLpId ZIP=".$zipAbs);
+
+ // Try to resolve currentDir from the BACKUP (folder or ZIP)
+ $currentDir = '';
+ $tmpExtractDir = '';
+ $bp = (string) ($this->course->backup_path ?? '');
+
+ // Case A: backup_path is an extracted directory
+ if ($bp && is_dir($bp)) {
+ try {
+ $rii = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($bp, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+ foreach ($rii as $f) {
+ if ($f->isFile() && strtolower($f->getFilename()) === 'imsmanifest.xml') {
+ $currentDir = $f->getPath();
+ break;
+ }
+ }
+ } catch (\Throwable $e) {
+ error_log($logp.'Scan BACKUP dir failed: '.$e->getMessage());
+ }
}
- }
- if (isset($lp->extraFields) && !empty($lp->extraFields)) {
- $extraFieldValue = new ExtraFieldValue('lp');
- foreach ($lp->extraFields as $extraField) {
- $params = [
- 'item_id' => $new_lp_id,
- 'value' => $extraField['value'],
- 'variable' => $extraField['variable'],
- ];
- $extraFieldValue->save($params);
+ // Case B: backup_path is a ZIP under var/cache/course_backups
+ if (!$currentDir && $bp && is_file($bp) && preg_match('/\.zip$/i', $bp)) {
+ $tmpExtractDir = rtrim(sys_get_temp_dir(), '/').'/scorm_restore_'.uniqid('', true);
+ @mkdir($tmpExtractDir, 0777, true);
+ try {
+ $zf = new ZipFile();
+ $zf->openFile($bp);
+ $zf->extractTo($tmpExtractDir);
+ $zf->close();
+
+ $rii = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($tmpExtractDir, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+ foreach ($rii as $f) {
+ if ($f->isFile() && strtolower($f->getFilename()) === 'imsmanifest.xml') {
+ $currentDir = $f->getPath();
+ break;
+ }
+ }
+ } catch (\Throwable $e) {
+ error_log($logp.'TMP unzip failed: '.$e->getMessage());
+ }
}
- }
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_LEARNPATH,
- $new_lp_id,
- 'LearnpathAdded',
- api_get_user_id(),
- 0,
- 0,
- 0,
- 0,
- $session_id
- );*/
-
- // Set the new LP to visible
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_LEARNPATH,
- $new_lp_id,
- $defaultLpVisibility,
- api_get_user_id(),
- 0,
- 0,
- 0,
- 0,
- $session_id
- );*/
-
- $new_item_ids = [];
- $parent_item_ids = [];
- $previous_item_ids = [];
- $next_item_ids = [];
- $old_prerequisite = [];
- $old_refs = [];
- $prerequisite_ids = [];
-
- foreach ($lp->get_items() as $index => $item) {
- // we set the ref code here and then we update in a for loop
- $ref = $item['ref'];
-
- // Dealing with path the same way as ref as some data has
- // been put into path when it's a local resource
- // Only fix the path for no scos
- if ('sco' === $item['item_type']) {
- $path = $item['path'];
+
+ if ($currentDir) {
+ error_log($logp.'Resolved currentDir from BACKUP: '.$currentDir);
} else {
- $path = $this->get_new_id($item['item_type'], $item['path']);
+ error_log($logp.'Could not resolve currentDir from backup; import_package will derive it');
}
- $item['item_type'] = 'dokeos_chapter' == $item['item_type'] ? 'dir' : $item['item_type'];
+ // Import in scorm class (import_manifest will create LP + items)
+ $sc = new \scorm();
+ $fileInfo = ['tmp_name' => $zipAbs, 'name' => basename($zipAbs)];
+
+ $ok = $sc->import_package($fileInfo, $currentDir);
- $masteryScore = $item['mastery_score'];
- // If item is a chamilo quiz, then use the max score as mastery_score
- if ('quiz' == $item['item_type']) {
- if (empty($masteryScore)) {
- $masteryScore = $item['max_score'];
+ // Cleanup tmp if we extracted the backup ZIP
+ if ($tmpExtractDir && is_dir($tmpExtractDir)) {
+ $it = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($tmpExtractDir, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+ foreach ($it as $p) {
+ $p->isDir() ? @rmdir($p->getPathname()) : @unlink($p->getPathname());
}
+ @rmdir($tmpExtractDir);
}
- $prerequisiteMinScore = $item['prerequisite_min_score'] ?? null;
- $prerequisiteMaxScore = $item['prerequisite_max_score'] ?? null;
- $params = [
- 'c_id' => $this->destination_course_id,
- 'lp_id' => self::DBUTF8($new_lp_id),
- 'item_type' => self::DBUTF8($item['item_type']),
- 'ref' => self::DBUTF8($ref),
- 'path' => self::DBUTF8($path),
- 'title' => self::DBUTF8($item['title']),
- 'description' => self::DBUTF8($item['description']),
- 'min_score' => self::DBUTF8($item['min_score']),
- 'max_score' => self::DBUTF8($item['max_score']),
- 'mastery_score' => self::DBUTF8($masteryScore),
- 'prerequisite_min_score' => $prerequisiteMinScore,
- 'prerequisite_max_score' => $prerequisiteMaxScore,
- 'parent_item_id' => self::DBUTF8($item['parent_item_id']),
- 'previous_item_id' => self::DBUTF8($item['previous_item_id']),
- 'next_item_id' => self::DBUTF8($item['next_item_id']),
- 'display_order' => self::DBUTF8($item['display_order']),
- 'prerequisite' => self::DBUTF8($item['prerequisite']),
- 'parameters' => self::DBUTF8($item['parameters']),
- 'audio' => self::DBUTF8($item['audio']),
- 'launch_data' => self::DBUTF8($item['launch_data']),
- ];
+ if ($ok !== true) {
+ error_log($logp."import_package() returned false");
+ } else {
+ if (empty($sc->manifestToString)) {
+ error_log($logp."manifestToString empty after import_package()");
+ } else {
+ // Parse & import manifest (creates LP + items)
+ $sc->parse_manifest();
+
+ /** @var CLp|null $lp */
+ $lp = $sc->import_manifest($courseId, 1, (int) $session_id);
+ if ($lp instanceof CLp) {
+ if (property_exists($lpObj, 'content_local')) {
+ $lp->setContentLocal((int) $lpObj->content_local);
+ }
+ if (property_exists($lpObj, 'content_maker')) {
+ $lp->setContentMaker((string) $lpObj->content_maker);
+ }
+ $lp->setDefaultEncoding((string) $encoding);
- $new_item_id = Database::insert($table_item, $params);
- if ($new_item_id) {
- $sql = "UPDATE $table_item SET id = iid WHERE iid = $new_item_id";
- Database::query($sql);
-
- //save a link between old and new item IDs
- $new_item_ids[$item['id']] = $new_item_id;
- //save a reference of items that need a parent_item_id refresh
- $parent_item_ids[$new_item_id] = $item['parent_item_id'];
- //save a reference of items that need a previous_item_id refresh
- $previous_item_ids[$new_item_id] = $item['previous_item_id'];
- //save a reference of items that need a next_item_id refresh
- $next_item_ids[$new_item_id] = $item['next_item_id'];
-
- if (!empty($item['prerequisite'])) {
- if ('2' == $lp->lp_type) {
- // if is an sco
- $old_prerequisite[$new_item_id] = $item['prerequisite'];
- } else {
- $old_prerequisite[$new_item_id] = isset($new_item_ids[$item['prerequisite']]) ? $new_item_ids[$item['prerequisite']] : '';
- }
- }
+ $em->persist($lp);
+ $em->flush();
- if (!empty($ref)) {
- if ('2' == $lp->lp_type) {
- // if is an sco
- $old_refs[$new_item_id] = $ref;
- } elseif (isset($new_item_ids[$ref])) {
- $old_refs[$new_item_id] = $new_item_ids[$ref];
+ $createdLpId = (int)$lp->getIid();
+ if ($lpBucketKey !== null && isset($this->course->resources[$lpBucketKey][$srcLpId])) {
+ $this->course->resources[$lpBucketKey][$srcLpId]->destination_id = $createdLpId;
+ }
+ error_log($logp."SCORM LP created id=".$createdLpId." (via manifest)");
+ } else {
+ error_log($logp."import_manifest() returned NULL");
}
}
- $prerequisite_ids[$new_item_id] = $item['prerequisite'];
}
}
+ } catch (\Throwable $e) {
+ error_log($logp.'EXCEPTION: '.$e->getMessage());
+ } finally {
+ if (empty($createdLpId)) {
+ $lp = (new CLp())
+ ->setLpType(CLp::SCORM_TYPE)
+ ->setTitle((string) $lpName)
+ ->setDefaultEncoding((string) $encoding)
+ ->setJsLib('scorm_api.php')
+ ->setUseMaxScore(1)
+ ->setParent($courseEntity);
+
+ if (method_exists($lp, 'addCourseLink')) {
+ // pass session only if available
+ $lp->addCourseLink($courseEntity, $session ?: null);
+ }
- // Updating prerequisites
- foreach ($old_prerequisite as $key => $my_old_prerequisite) {
- if ('' != $my_old_prerequisite) {
- $my_old_prerequisite = Database::escape_string($my_old_prerequisite);
- $sql = "UPDATE $table_item SET prerequisite = '$my_old_prerequisite'
- WHERE c_id = ".$this->destination_course_id." AND id = '".$key."' ";
- Database::query($sql);
+ $lpRepo->createLp($lp);
+ $em->flush();
+
+ $createdLpId = (int) $lp->getIid();
+ if ($lpBucketKey !== null && isset($this->course->resources[$lpBucketKey][$srcLpId])) {
+ $this->course->resources[$lpBucketKey][$srcLpId]->destination_id = $createdLpId;
}
+ error_log($logp."SCORM LP created id=".$createdLpId." (FALLBACK)");
}
- // Updating refs
- foreach ($old_refs as $key => $my_old_ref) {
- if ('' != $my_old_ref) {
- $my_old_ref = Database::escape_string($my_old_ref);
- $sql = "UPDATE $table_item SET ref = '$my_old_ref'
- WHERE c_id = ".$this->destination_course_id." AND id = $key";
- Database::query($sql);
- }
+ // Remove temp ZIP if we created it in findScormPackageForLp()
+ if (!empty($zipTemp) && !empty($zipAbs) && is_file($zipAbs)) {
+ @unlink($zipAbs);
}
+ }
- foreach ($parent_item_ids as $new_item_id => $parent_item_old_id) {
- $new_item_id = (int) $new_item_id;
- $parent_new_id = 0;
- if (0 != $parent_item_old_id) {
- $parent_new_id = isset($new_item_ids[$parent_item_old_id]) ? $new_item_ids[$parent_item_old_id] : 0;
- }
+ continue; // next LP
+ }
- $parent_new_id = Database::escape_string($parent_new_id);
- $sql = "UPDATE $table_item SET parent_item_id = '$parent_new_id'
- WHERE c_id = ".$this->destination_course_id." AND id = $new_item_id";
- Database::query($sql);
- }
+ // ---- Non-SCORM ----
+ $lp = (new CLp())
+ ->setLpType(CLp::LP_TYPE)
+ ->setTitle((string) $lpName)
+ ->setDefaultEncoding((string) $encoding)
+ ->setJsLib('scorm_api.php')
+ ->setUseMaxScore(1)
+ ->setParent($courseEntity);
+
+ if (method_exists($lp, 'addCourseLink')) {
+ $lp->addCourseLink($courseEntity, $session ?: null);
+ }
- foreach ($previous_item_ids as $new_item_id => $previous_item_old_id) {
- $new_item_id = (int) $new_item_id;
- $previous_new_id = 0;
- if (0 != $previous_item_old_id) {
- $previous_new_id = isset($new_item_ids[$previous_item_old_id]) ? $new_item_ids[$previous_item_old_id] : 0;
- }
- $previous_new_id = Database::escape_string($previous_new_id);
- $sql = "UPDATE $table_item SET previous_item_id = '$previous_new_id'
- WHERE c_id = ".$this->destination_course_id." AND id = '".$new_item_id."'";
- Database::query($sql);
- }
+ $lpRepo->createLp($lp);
+ $em->flush();
+ error_log($logp."Standard LP created id=".$lp->getIid());
- foreach ($next_item_ids as $new_item_id => $next_item_old_id) {
- $new_item_id = (int) $new_item_id;
- $next_new_id = 0;
- if (0 != $next_item_old_id) {
- $next_new_id = isset($new_item_ids[$next_item_old_id]) ? $new_item_ids[$next_item_old_id] : 0;
- }
- $next_new_id = Database::escape_string($next_new_id);
- $sql = "UPDATE $table_item SET next_item_id = '$next_new_id'
- WHERE c_id = ".$this->destination_course_id." AND id = '".$new_item_id."'";
- Database::query($sql);
- }
+ if ($lpBucketKey !== null && isset($this->course->resources[$lpBucketKey][$srcLpId])) {
+ $this->course->resources[$lpBucketKey][$srcLpId]->destination_id = (int) $lp->getIid();
+ }
- foreach ($prerequisite_ids as $new_item_id => $prerequisite_old_id) {
- $new_item_id = (int) $new_item_id;
- $prerequisite_new_id = 0;
- if (0 != $prerequisite_old_id) {
- $prerequisite_new_id = $new_item_ids[$prerequisite_old_id];
- }
- $sql = "UPDATE $table_item SET prerequisite = $prerequisite_new_id
- WHERE c_id = ".$this->destination_course_id." AND id = $new_item_id";
- Database::query($sql);
- }
- $this->course->resources[RESOURCE_LEARNPATH][$id]->destination_id = $new_lp_id;
+ // Manual items (only for non-SCORM if present in backup)
+ if (!empty($lpObj->items) && is_array($lpObj->items)) {
+ $lpItemRepo = Container::getLpItemRepository();
+ $rootItem = $lpItemRepo->getRootItem($lp->getIid());
+ $parents = [0 => $rootItem];
+
+ foreach ($lpObj->items as $it) {
+ $level = (int) ($it['level'] ?? 0);
+ if (!isset($parents[$level])) { $parents[$level] = end($parents); }
+ $parentEntity = $parents[$level] ?? $rootItem;
+
+ $lpItem = (new CLpItem())
+ ->setTitle((string) ($it['title'] ?? ''))
+ ->setItemType((string) ($it['item_type'] ?? 'dir'))
+ ->setRef((string) ($it['identifier'] ?? ''))
+ ->setPath((string) ($it['path'] ?? ''))
+ ->setMinScore(0)
+ ->setMaxScore((int) ($it['max_score'] ?? 100))
+ ->setPrerequisite((string) ($it['prerequisites'] ?? ''))
+ ->setLaunchData((string) ($it['datafromlms'] ?? ''))
+ ->setParameters((string) ($it['parameters'] ?? ''))
+ ->setLp($lp)
+ ->setParent($parentEntity);
+
+ $lpItemRepo->create($lpItem);
+ $parents[$level+1] = $lpItem;
}
+ $em->flush();
+ error_log($logp."Standard LP id=".$lp->getIid()." items=".count($lpObj->items));
}
}
}
/**
- * Gets the new ID of one specific tool item from the tool name and the old ID.
- *
- * @param string Tool name
- * @param int Old ID
- *
- * @return int New ID
+ * Restore glossary.
*/
- public function get_new_id($tool, $ref)
+ public function restore_glossary($sessionId = 0)
{
- // Check if the value exist in the current array.
- if ('hotpotatoes' === $tool) {
- $tool = 'document';
+ if (!$this->course->has_resources(RESOURCE_GLOSSARY)) {
+ $this->debug && error_log('COURSE_DEBUG: restore_glossary: no glossary resources in backup.');
+ return;
}
- if ('student_publication' === $tool) {
- $tool = RESOURCE_WORK;
+ $em = Database::getManager();
+ /** @var CGlossaryRepository $repo */
+ $repo = $em->getRepository(CGlossary::class);
+ /** @var CourseEntity $courseEntity */
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $sessionId ? api_get_session_entity((int) $sessionId) : null;
+
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+ if ($backupRoot === '') {
+ $this->debug && error_log('COURSE_DEBUG: restore_glossary: backupRoot empty; URL rewriting may be partial.');
}
- if (isset($this->course->resources[$tool][$ref]) &&
- isset($this->course->resources[$tool][$ref]->destination_id) &&
- !empty($this->course->resources[$tool][$ref]->destination_id)
- ) {
- return $this->course->resources[$tool][$ref]->destination_id;
- }
+ $resources = $this->course->resources;
- // Check if the course is the same (last hope).
- if ($this->course_origin_id == $this->destination_course_id) {
- return $ref;
- }
+ foreach ($resources[RESOURCE_GLOSSARY] as $legacyId => $gls) {
+ try {
+ $title = (string) ($gls->name ?? $gls->title ?? '');
+ $desc = (string) ($gls->description ?? '');
+ $order = (int) ($gls->display_order ?? 0);
+
+ $desc = ChamiloHelper::rewriteLegacyCourseUrlsToAssets($desc, $courseEntity, $backupRoot) ?? $desc;
+
+ $existing = null;
+ if (method_exists($repo, 'getResourcesByCourse')) {
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity)
+ ->andWhere('resource.title = :title')
+ ->setParameter('title', $title)
+ ->setMaxResults(1);
+ $existing = $qb->getQuery()->getOneOrNullResult();
+ } else {
+ $existing = $repo->findOneBy(['title' => $title]);
+ }
- return '';
- }
+ if ($existing instanceof CGlossary) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $this->course->resources[RESOURCE_GLOSSARY][$legacyId]->destination_id = (int)$existing->getIid();
+ $this->debug && error_log("COURSE_DEBUG: restore_glossary: term exists title='{$title}' (skip).");
+ continue 2;
- /**
- * Restore glossary.
- */
- public function restore_glossary($sessionId = 0)
- {
- $sessionId = (int) $sessionId;
- if ($this->course->has_resources(RESOURCE_GLOSSARY)) {
- $table_glossary = Database::get_course_table(TABLE_GLOSSARY);
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_GLOSSARY] as $id => $glossary) {
- $params = [];
- if (!empty($sessionId)) {
- $params['session_id'] = $sessionId;
- }
+ case FILE_RENAME:
+ $base = $title === '' ? 'Glossary term' : $title;
+ $try = $base;
+ $i = 1;
+ $isTaken = static function($repo, $courseEntity, $sessionEntity, $titleTry) {
+ if (method_exists($repo, 'getResourcesByCourse')) {
+ $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity)
+ ->andWhere('resource.title = :t')->setParameter('t', $titleTry)
+ ->setMaxResults(1);
+ return (bool)$qb->getQuery()->getOneOrNullResult();
+ }
+ return (bool)$repo->findOneBy(['title' => $titleTry]);
+ };
+ while ($isTaken($repo, $courseEntity, $sessionEntity, $try)) {
+ $try = $base.' ('.($i++).')';
+ }
+ $title = $try;
+ $this->debug && error_log("COURSE_DEBUG: restore_glossary: renaming to '{$title}'.");
+ break;
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $glossary->description = DocumentManager::replaceUrlWithNewCourseCode(
- $glossary->description,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ case FILE_OVERWRITE:
+ $em->remove($existing);
+ $em->flush();
+ $this->debug && error_log("COURSE_DEBUG: restore_glossary: existing term deleted (overwrite).");
+ break;
- $params['c_id'] = $this->destination_course_id;
- $params['description'] = false === $glossary->description ? '' : self::DBUTF8($glossary->description);
- $params['display_order'] = $glossary->display_order;
- $params['name'] = self::DBUTF8($glossary->name);
- $params['glossary_id'] = 0;
- $my_id = Database::insert($table_glossary, $params);
- if ($my_id) {
- $sql = "UPDATE $table_glossary SET glossary_id = iid WHERE iid = $my_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_GLOSSARY,
- $my_id,
- 'GlossaryAdded',
- api_get_user_id()
- );*/
-
- if (!isset($this->course->resources[RESOURCE_GLOSSARY][$id])) {
- $this->course->resources[RESOURCE_GLOSSARY][$id] = new stdClass();
+ default:
+ $this->course->resources[RESOURCE_GLOSSARY][$legacyId]->destination_id = (int)$existing->getIid();
+ continue 2;
}
+ }
+
+ $entity = new CGlossary();
+ $entity
+ ->setTitle($title)
+ ->setDescription($desc);
+
+ if (method_exists($entity, 'setParent')) {
+ $entity->setParent($courseEntity);
+ }
+
+ if (method_exists($entity, 'addCourseLink')) {
+ $entity->addCourseLink($courseEntity, $sessionEntity);
+ }
+
+ if (method_exists($repo, 'create')) {
+ $repo->create($entity);
+ } else {
+ $em->persist($entity);
+ $em->flush();
+ }
+
+ if ($order && method_exists($entity, 'setDisplayOrder')) {
+ $entity->setDisplayOrder($order);
+ $em->flush();
+ }
- $this->course->resources[RESOURCE_GLOSSARY][$id]->destination_id = $my_id;
+ $newId = (int)$entity->getIid();
+ if (!isset($this->course->resources[RESOURCE_GLOSSARY][$legacyId])) {
+ $this->course->resources[RESOURCE_GLOSSARY][$legacyId] = new \stdClass();
}
+ $this->course->resources[RESOURCE_GLOSSARY][$legacyId]->destination_id = $newId;
+
+ $this->debug && error_log("COURSE_DEBUG: restore_glossary: created term iid={$newId}, title='{$title}'");
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_glossary: failed: '.$e->getMessage());
+ continue;
}
}
}
@@ -3145,81 +3638,217 @@ public function restore_glossary($sessionId = 0)
*/
public function restore_wiki($sessionId = 0)
{
- if ($this->course->has_resources(RESOURCE_WIKI)) {
- // wiki table of the target course
- $table_wiki = Database::get_course_table(TABLE_WIKI);
- $table_wiki_conf = Database::get_course_table(TABLE_WIKI_CONF);
+ if (!$this->course->has_resources(RESOURCE_WIKI)) {
+ $this->debug && error_log('COURSE_DEBUG: restore_wiki: no wiki resources in backup.');
+ return;
+ }
- // storing all the resources that have to be copied in an array
- $resources = $this->course->resources;
+ $em = Database::getManager();
+ /** @var CWikiRepository $repo */
+ $repo = $em->getRepository(CWiki::class);
+ /** @var CourseEntity $courseEntity */
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $sessionId ? api_get_session_entity((int)$sessionId) : null;
+
+ $cid = (int)$this->destination_course_id;
+ $sid = (int)($sessionEntity?->getId() ?? 0);
+
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+ if ($backupRoot === '') {
+ $this->debug && error_log('COURSE_DEBUG: restore_wiki: backupRoot empty; URL rewriting may be partial.');
+ }
+
+ $resources = $this->course->resources;
+
+ foreach ($resources[RESOURCE_WIKI] as $legacyId => $w) {
+ try {
+ $rawTitle = (string)($w->title ?? $w->name ?? '');
+ $reflink = (string)($w->reflink ?? '');
+ $content = (string)($w->content ?? '');
+ $comment = (string)($w->comment ?? '');
+ $progress = (string)($w->progress ?? '');
+ $version = (int) ($w->version ?? 1);
+ $groupId = (int) ($w->group_id ?? 0);
+ $userId = (int) ($w->user_id ?? api_get_user_id());
+ $dtimeStr = (string)($w->dtime ?? '');
+ $dtime = null;
+ try { $dtime = $dtimeStr !== '' ? new \DateTime($dtimeStr) : new \DateTime('now', new \DateTimeZone('UTC')); }
+ catch (\Throwable) { $dtime = new \DateTime('now', new \DateTimeZone('UTC')); }
+
+ $content = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $content,
+ $courseEntity,
+ $backupRoot
+ ) ?? $content;
+
+ if ($rawTitle === '') {
+ $rawTitle = 'Wiki page';
+ }
+ if ($content === '') {
+ $content = '
';
+ }
+
+ $makeSlug = static function (string $s): string {
+ $s = strtolower(trim($s));
+ $s = preg_replace('/[^\p{L}\p{N}]+/u', '-', $s) ?: '';
+ $s = trim($s, '-');
+ return $s === '' ? 'page' : $s;
+ };
+ $reflink = $reflink !== '' ? $makeSlug($reflink) : $makeSlug($rawTitle);
+
+ $qbExists = $repo->createQueryBuilder('w')
+ ->select('w.iid')
+ ->andWhere('w.cId = :cid')->setParameter('cid', $cid)
+ ->andWhere('w.reflink = :r')->setParameter('r', $reflink)
+ ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', $groupId);
+ if ($sid > 0) {
+ $qbExists->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', $sid);
+ } else {
+ $qbExists->andWhere('COALESCE(w.sessionId,0) = 0');
+ }
+ $exists = (bool)$qbExists->getQuery()->getOneOrNullResult();
+
+ if ($exists) {
+ switch ($this->file_option) {
+ case FILE_SKIP:
+ $qbLast = $repo->createQueryBuilder('w')
+ ->andWhere('w.cId = :cid')->setParameter('cid', $cid)
+ ->andWhere('w.reflink = :r')->setParameter('r', $reflink)
+ ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', $groupId)
+ ->orderBy('w.version', 'DESC')->setMaxResults(1);
+ if ($sid > 0) { $qbLast->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', $sid); }
+ else { $qbLast->andWhere('COALESCE(w.sessionId,0) = 0'); }
+
+ /** @var CWiki|null $last */
+ $last = $qbLast->getQuery()->getOneOrNullResult();
+ $dest = $last ? (int)($last->getPageId() ?: $last->getIid()) : 0;
+ $this->course->resources[RESOURCE_WIKI][$legacyId]->destination_id = $dest;
+ $this->debug && error_log("COURSE_DEBUG: restore_wiki: reflink '{$reflink}' exists → skip (page_id={$dest}).");
+ continue 2;
+
+ case FILE_RENAME:
+ $baseSlug = $reflink;
+ $baseTitle = $rawTitle;
+ $i = 1;
+ $trySlug = $baseSlug.'-'.$i;
+ $isTaken = function (string $slug) use ($repo, $cid, $sid, $groupId): bool {
+ $qb = $repo->createQueryBuilder('w')
+ ->select('w.iid')
+ ->andWhere('w.cId = :cid')->setParameter('cid', $cid)
+ ->andWhere('w.reflink = :r')->setParameter('r', $slug)
+ ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', $groupId);
+ if ($sid > 0) $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', $sid);
+ else $qb->andWhere('COALESCE(w.sessionId,0) = 0');
+ $qb->setMaxResults(1);
+ return (bool)$qb->getQuery()->getOneOrNullResult();
+ };
+ while ($isTaken($trySlug)) { $trySlug = $baseSlug.'-'.(++$i); }
+ $reflink = $trySlug;
+ $rawTitle = $baseTitle.' ('.$i.')';
+ $this->debug && error_log("COURSE_DEBUG: restore_wiki: renamed reflink to '{$reflink}' / title='{$rawTitle}'.");
+ break;
+
+ case FILE_OVERWRITE:
+ $qbAll = $repo->createQueryBuilder('w')
+ ->andWhere('w.cId = :cid')->setParameter('cid', $cid)
+ ->andWhere('w.reflink = :r')->setParameter('r', $reflink)
+ ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', $groupId);
+ if ($sid > 0) $qbAll->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', $sid);
+ else $qbAll->andWhere('COALESCE(w.sessionId,0) = 0');
+
+ foreach ($qbAll->getQuery()->getResult() as $old) {
+ $em->remove($old);
+ }
+ $em->flush();
+ $this->debug && error_log("COURSE_DEBUG: restore_wiki: removed previous pages for reflink '{$reflink}' (overwrite).");
+ break;
- foreach ($resources[RESOURCE_WIKI] as $id => $wiki) {
- // the sql statement to insert the groups from the old course to the new course
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $wiki->content = DocumentManager::replaceUrlWithNewCourseCode(
- $wiki->content,
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ default:
+ $this->debug && error_log("COURSE_DEBUG: restore_wiki: unknown file_option → skip.");
+ continue 2;
+ }
+ }
- $params = [
- 'c_id' => $this->destination_course_id,
- 'page_id' => self::DBUTF8($wiki->page_id),
- 'reflink' => self::DBUTF8($wiki->reflink),
- 'title' => self::DBUTF8($wiki->title),
- 'content' => false === $wiki->content ? '' : self::DBUTF8($wiki->content),
- 'user_id' => (int) ($wiki->user_id),
- 'group_id' => (int) ($wiki->group_id),
- 'dtime' => self::DBUTF8($wiki->dtime),
- 'progress' => self::DBUTF8($wiki->progress),
- 'version' => (int) ($wiki->version),
- 'session_id' => !empty($session_id) ? (int) $session_id : 0,
- 'addlock' => 0,
- 'editlock' => 0,
- 'visibility' => 0,
- 'addlock_disc' => 0,
- 'visibility_disc' => 0,
- 'ratinglock_disc' => 0,
- 'assignment' => 0,
- 'comment' => '',
- 'is_editing' => 0,
- 'linksto' => 0,
- 'tag' => '',
- 'user_ip' => '',
- ];
+ $wiki = new CWiki();
+ $wiki->setCId($cid);
+ $wiki->setSessionId($sid);
+ $wiki->setGroupId($groupId);
+ $wiki->setReflink($reflink);
+ $wiki->setTitle($rawTitle);
+ $wiki->setContent($content);
+ $wiki->setComment($comment);
+ $wiki->setProgress($progress);
+ $wiki->setVersion($version > 0 ? $version : 1);
+ $wiki->setUserId($userId);
+ $wiki->setDtime($dtime);
+ $wiki->setIsEditing(0);
+ $wiki->setTimeEdit(null);
+ $wiki->setHits((int) ($w->hits ?? 0));
+ $wiki->setAddlock((int) ($w->addlock ?? 1));
+ $wiki->setEditlock((int) ($w->editlock ?? 0));
+ $wiki->setVisibility((int) ($w->visibility ?? 1));
+ $wiki->setAddlockDisc((int) ($w->addlock_disc ?? 1));
+ $wiki->setVisibilityDisc((int) ($w->visibility_disc ?? 1));
+ $wiki->setRatinglockDisc((int) ($w->ratinglock_disc ?? 1));
+ $wiki->setAssignment((int) ($w->assignment ?? 0));
+ $wiki->setScore(isset($w->score) ? (int) $w->score : 0);
+ $wiki->setLinksto((string) ($w->linksto ?? ''));
+ $wiki->setTag((string) ($w->tag ?? ''));
+ $wiki->setUserIp((string) ($w->user_ip ?? api_get_real_ip()));
+
+ if (method_exists($wiki, 'setParent')) {
+ $wiki->setParent($courseEntity);
+ }
+ if (method_exists($wiki, 'setCreator')) {
+ $wiki->setCreator(api_get_user_entity());
+ }
+ $groupEntity = $groupId ? api_get_group_entity($groupId) : null;
+ if (method_exists($wiki, 'addCourseLink')) {
+ $wiki->addCourseLink($courseEntity, $sessionEntity, $groupEntity);
+ }
- $new_id = Database::insert($table_wiki, $params);
-
- if ($new_id) {
- $sql = "UPDATE $table_wiki SET page_id = '$new_id', id = iid
- WHERE c_id = ".$this->destination_course_id." AND iid = '$new_id'";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_WIKI][$id]->destination_id = $new_id;
-
- // we also add an entry in wiki_conf
- $params = [
- 'c_id' => $this->destination_course_id,
- 'page_id' => $new_id,
- 'task' => '',
- 'feedback1' => '',
- 'feedback2' => '',
- 'feedback3' => '',
- 'fprogress1' => '',
- 'fprogress2' => '',
- 'fprogress3' => '',
- 'max_size' => 0,
- 'max_text' => 0,
- 'max_version' => 0,
- 'startdate_assig' => null,
- 'enddate_assig' => null,
- 'delayedsubmit' => 0,
- ];
+ $em->persist($wiki);
+ $em->flush();
- Database::insert($table_wiki_conf, $params);
+ if (empty($w->page_id)) {
+ $wiki->setPageId((int) $wiki->getIid());
+ $em->flush();
+ } else {
+ $pid = (int) $w->page_id;
+ $wiki->setPageId($pid > 0 ? $pid : (int) $wiki->getIid());
+ $em->flush();
}
+
+ $conf = new CWikiConf();
+ $conf->setCId($cid);
+ $conf->setPageId((int) $wiki->getPageId());
+ $conf->setTask((string) ($w->task ?? ''));
+ $conf->setFeedback1((string) ($w->feedback1 ?? ''));
+ $conf->setFeedback2((string) ($w->feedback2 ?? ''));
+ $conf->setFeedback3((string) ($w->feedback3 ?? ''));
+ $conf->setFprogress1((string) ($w->fprogress1 ?? ''));
+ $conf->setFprogress2((string) ($w->fprogress2 ?? ''));
+ $conf->setFprogress3((string) ($w->fprogress3 ?? ''));
+ $conf->setMaxText(isset($w->max_text) ? (int) $w->max_text : 0);
+ $conf->setMaxVersion(isset($w->max_version) ? (int) $w->max_version : 0);
+ try {
+ $conf->setStartdateAssig(!empty($w->startdate_assig) ? new \DateTime((string) $w->startdate_assig) : null);
+ } catch (\Throwable) { $conf->setStartdateAssig(null); }
+ try {
+ $conf->setEnddateAssig(!empty($w->enddate_assig) ? new \DateTime((string) $w->enddate_assig) : null);
+ } catch (\Throwable) { $conf->setEnddateAssig(null); }
+ $conf->setDelayedsubmit(isset($w->delayedsubmit) ? (int) $w->delayedsubmit : 0);
+
+ $em->persist($conf);
+ $em->flush();
+
+ $this->course->resources[RESOURCE_WIKI][$legacyId]->destination_id = (int) $wiki->getPageId();
+
+ $this->debug && error_log("COURSE_DEBUG: restore_wiki: created page iid=".(int) $wiki->getIid()." page_id=".(int) $wiki->getPageId()." reflink='{$reflink}'");
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_wiki: failed: '.$e->getMessage());
+ continue;
}
}
}
@@ -3231,87 +3860,148 @@ public function restore_wiki($sessionId = 0)
*/
public function restore_thematic($sessionId = 0)
{
- if ($this->course->has_resources(RESOURCE_THEMATIC)) {
- $table_thematic = Database::get_course_table(TABLE_THEMATIC);
- $table_thematic_advance = Database::get_course_table(TABLE_THEMATIC_ADVANCE);
- $table_thematic_plan = Database::get_course_table(TABLE_THEMATIC_PLAN);
+ if (!$this->course->has_resources(RESOURCE_THEMATIC)) {
+ $this->debug && error_log('COURSE_DEBUG: restore_thematic: no thematic resources.');
+ return;
+ }
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_THEMATIC] as $id => $thematic) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $thematic->params['content'] = DocumentManager::replaceUrlWithNewCourseCode(
- $thematic->params['content'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
- $thematic->params['c_id'] = $this->destination_course_id;
- unset($thematic->params['id']);
- unset($thematic->params['iid']);
-
- $last_id = Database::insert($table_thematic, $thematic->params, false);
-
- if ($last_id) {
- $sql = "UPDATE $table_thematic SET id = iid WHERE iid = $last_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- 'thematic',
- $last_id,
- 'ThematicAdded',
- api_get_user_id()
- );*/
-
- foreach ($thematic->thematic_advance_list as $thematic_advance) {
- unset($thematic_advance['id']);
- unset($thematic_advance['iid']);
- $thematic_advance['attendance_id'] = 0;
- $thematic_advance['thematic_id'] = $last_id;
- $thematic_advance['c_id'] = $this->destination_course_id;
-
- $my_id = Database::insert(
- $table_thematic_advance,
- $thematic_advance,
- false
- );
+ $em = Database::getManager();
+ /** @var CourseEntity $courseEntity */
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $sessionId ? api_get_session_entity((int)$sessionId) : null;
+
+ $cid = (int)$this->destination_course_id;
+ $sid = (int)($sessionEntity?->getId() ?? 0);
+
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+
+ $resources = $this->course->resources;
+
+ foreach ($resources[RESOURCE_THEMATIC] as $legacyId => $t) {
+ try {
+ $p = (array)($t->params ?? []);
+ $title = trim((string)($p['title'] ?? $p['name'] ?? ''));
+ $content = (string)($p['content'] ?? '');
+ $active = (bool) ($p['active'] ?? true);
+
+ if ($content !== '') {
+ $content = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $content,
+ $courseEntity,
+ $backupRoot
+ ) ?? $content;
+ }
+
+ if ($title === '') {
+ $title = 'Thematic';
+ }
+
+ $thematic = new CThematic();
+ $thematic
+ ->setTitle($title)
+ ->setContent($content)
+ ->setActive($active);
+
+ if (method_exists($thematic, 'setParent')) {
+ $thematic->setParent($courseEntity);
+ }
+ if (method_exists($thematic, 'setCreator')) {
+ $thematic->setCreator(api_get_user_entity());
+ }
+ if (method_exists($thematic, 'addCourseLink')) {
+ $thematic->addCourseLink($courseEntity, $sessionEntity);
+ }
- if ($my_id) {
- $sql = "UPDATE $table_thematic_advance SET id = iid WHERE iid = $my_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- 'thematic_advance',
- $my_id,
- 'ThematicAdvanceAdded',
- api_get_user_id()
- );*/
+ $em->persist($thematic);
+ $em->flush();
+
+ $this->course->resources[RESOURCE_THEMATIC][$legacyId]->destination_id = (int)$thematic->getIid();
+
+ $advList = (array)($t->thematic_advance_list ?? []);
+ foreach ($advList as $adv) {
+ if (!is_array($adv)) { $adv = (array)$adv; }
+
+ $advContent = (string)($adv['content'] ?? '');
+ if ($advContent !== '') {
+ $advContent = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $advContent,
+ $courseEntity,
+ $backupRoot
+ ) ?? $advContent;
+ }
+
+ $startStr = (string)($adv['start_date'] ?? $adv['startDate'] ?? '');
+ try {
+ $startDate = $startStr !== '' ? new \DateTime($startStr) : new \DateTime('now', new \DateTimeZone('UTC'));
+ } catch (\Throwable) {
+ $startDate = new \DateTime('now', new \DateTimeZone('UTC'));
+ }
+
+ $duration = (int)($adv['duration'] ?? 1);
+ $doneAdvance = (bool)($adv['done_advance'] ?? $adv['doneAdvance'] ?? false);
+
+ $advance = new CThematicAdvance();
+ $advance
+ ->setThematic($thematic)
+ ->setContent($advContent)
+ ->setStartDate($startDate)
+ ->setDuration($duration)
+ ->setDoneAdvance($doneAdvance);
+
+ $attId = (int)($adv['attendance_id'] ?? 0);
+ if ($attId > 0) {
+ $att = $em->getRepository(CAttendance::class)->find($attId);
+ if ($att) {
+ $advance->setAttendance($att);
}
}
- foreach ($thematic->thematic_plan_list as $thematic_plan) {
- unset($thematic_plan['id']);
- unset($thematic_plan['iid']);
- $thematic_plan['thematic_id'] = $last_id;
- $thematic_plan['c_id'] = $this->destination_course_id;
- $my_id = Database::insert($table_thematic_plan, $thematic_plan, false);
-
- if ($my_id) {
- $sql = "UPDATE $table_thematic_plan SET id = iid WHERE iid = $my_id";
- Database::query($sql);
-
- /*api_item_property_update(
- $this->destination_course_info,
- 'thematic_plan',
- $my_id,
- 'ThematicPlanAdded',
- api_get_user_id()
- );*/
+ $roomId = (int)($adv['room_id'] ?? 0);
+ if ($roomId > 0) {
+ $room = $em->getRepository(Room::class)->find($roomId);
+ if ($room) {
+ $advance->setRoom($room);
}
}
+
+ $em->persist($advance);
+ }
+
+ $planList = (array)($t->thematic_plan_list ?? []);
+ foreach ($planList as $pl) {
+ if (!is_array($pl)) { $pl = (array)$pl; }
+
+ $plTitle = trim((string)($pl['title'] ?? ''));
+ if ($plTitle === '') { $plTitle = 'Plan'; }
+
+ $plDesc = (string)($pl['description'] ?? '');
+ if ($plDesc !== '') {
+ $plDesc = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $plDesc,
+ $courseEntity,
+ $backupRoot
+ ) ?? $plDesc;
+ }
+
+ $descType = (int)($pl['description_type'] ?? $pl['descriptionType'] ?? 0);
+
+ $plan = new CThematicPlan();
+ $plan
+ ->setThematic($thematic)
+ ->setTitle($plTitle)
+ ->setDescription($plDesc)
+ ->setDescriptionType($descType);
+
+ $em->persist($plan);
}
+
+ $em->flush();
+
+ $this->debug && error_log("COURSE_DEBUG: restore_thematic: created thematic iid=".(int)$thematic->getIid()." (advances=".count($advList).", plans=".count($planList).")");
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_thematic: failed: '.$e->getMessage());
+ continue;
}
}
}
@@ -3323,55 +4013,110 @@ public function restore_thematic($sessionId = 0)
*/
public function restore_attendance($sessionId = 0)
{
- if ($this->course->has_resources(RESOURCE_ATTENDANCE)) {
- $table_attendance = Database::get_course_table(TABLE_ATTENDANCE);
- $table_attendance_calendar = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR);
+ if (!$this->course->has_resources(RESOURCE_ATTENDANCE)) {
+ $this->debug && error_log('COURSE_DEBUG: restore_attendance: no attendance resources.');
+ return;
+ }
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_ATTENDANCE] as $id => $obj) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $obj->params['description'] = DocumentManager::replaceUrlWithNewCourseCode(
- $obj->params['description'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
- );
+ $em = Database::getManager();
+ /** @var CourseEntity $courseEntity */
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $sessionId ? api_get_session_entity((int)$sessionId) : null;
- unset($obj->params['id']);
- unset($obj->params['iid']);
- $obj->params['c_id'] = $this->destination_course_id;
- $last_id = Database::insert($table_attendance, $obj->params);
-
- if (is_numeric($last_id)) {
- $sql = "UPDATE $table_attendance SET id = iid WHERE iid = $last_id";
- Database::query($sql);
-
- $this->course->resources[RESOURCE_ATTENDANCE][$id]->destination_id = $last_id;
-
- /*api_item_property_update(
- $this->destination_course_info,
- TOOL_ATTENDANCE,
- $last_id,
- 'AttendanceAdded',
- api_get_user_id()
- );*/
-
- foreach ($obj->attendance_calendar as $attendance_calendar) {
- unset($attendance_calendar['id']);
- unset($attendance_calendar['iid']);
-
- $attendance_calendar['attendance_id'] = $last_id;
- $attendance_calendar['c_id'] = $this->destination_course_id;
- $attendanceCalendarId = Database::insert(
- $table_attendance_calendar,
- $attendance_calendar
- );
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+
+ $resources = $this->course->resources;
+
+ foreach ($resources[RESOURCE_ATTENDANCE] as $legacyId => $att) {
+ try {
+ $p = (array)($att->params ?? []);
+
+ $title = trim((string)($p['title'] ?? 'Attendance'));
+ $desc = (string)($p['description'] ?? '');
+ $active = (int)($p['active'] ?? 1);
+
+ if ($desc !== '') {
+ $desc = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $desc,
+ $courseEntity,
+ $backupRoot
+ ) ?? $desc;
+ }
+
+ $qualTitle = isset($p['attendance_qualify_title']) ? (string)$p['attendance_qualify_title'] : null;
+ $qualMax = (int)($p['attendance_qualify_max'] ?? 0);
+ $weight = (float)($p['attendance_weight'] ?? 0.0);
+ $locked = (int)($p['locked'] ?? 0);
+
+ $a = new CAttendance();
+ $a->setTitle($title)
+ ->setDescription($desc)
+ ->setActive($active)
+ ->setAttendanceQualifyTitle($qualTitle ?? '')
+ ->setAttendanceQualifyMax($qualMax)
+ ->setAttendanceWeight($weight)
+ ->setLocked($locked);
+
+ if (method_exists($a, 'setParent')) {
+ $a->setParent($courseEntity);
+ }
+ if (method_exists($a, 'setCreator')) {
+ $a->setCreator(api_get_user_entity());
+ }
+ if (method_exists($a, 'addCourseLink')) {
+ $a->addCourseLink($courseEntity, $sessionEntity);
+ }
+
+ $em->persist($a);
+ $em->flush();
+
+ $this->course->resources[RESOURCE_ATTENDANCE][$legacyId]->destination_id = (int)$a->getIid();
+
+ $calList = (array)($att->attendance_calendar ?? []);
+ foreach ($calList as $c) {
+ if (!is_array($c)) { $c = (array)$c; }
- $sql = "UPDATE $table_attendance_calendar SET id = iid WHERE iid = $attendanceCalendarId";
- Database::query($sql);
+ $rawDt = (string)($c['date_time'] ?? $c['dateTime'] ?? $c['start_date'] ?? '');
+ try {
+ $dt = $rawDt !== '' ? new \DateTime($rawDt) : new \DateTime('now', new \DateTimeZone('UTC'));
+ } catch (\Throwable) {
+ $dt = new \DateTime('now', new \DateTimeZone('UTC'));
+ }
+
+ $done = (bool)($c['done_attendance'] ?? $c['doneAttendance'] ?? false);
+ $blocked = (bool)($c['blocked'] ?? false);
+ $duration = isset($c['duration']) ? (int)$c['duration'] : null;
+
+ $cal = new CAttendanceCalendar();
+ $cal->setAttendance($a)
+ ->setDateTime($dt)
+ ->setDoneAttendance($done)
+ ->setBlocked($blocked)
+ ->setDuration($duration);
+
+ $em->persist($cal);
+ $em->flush();
+
+ $groupId = (int)($c['group_id'] ?? 0);
+ if ($groupId > 0) {
+ try {
+ $repo = $em->getRepository(CAttendanceCalendarRelGroup::class);
+ if (method_exists($repo, 'addGroupToCalendar')) {
+ $repo->addGroupToCalendar((int)$cal->getIid(), $groupId);
+ }
+ } catch (\Throwable $e) {
+ $this->debug && error_log('COURSE_DEBUG: restore_attendance: calendar group link skipped: '.$e->getMessage());
+ }
}
}
+
+ $em->flush();
+ $this->debug && error_log('COURSE_DEBUG: restore_attendance: created attendance iid='.(int)$a->getIid().' (cal='.count($calList).')');
+
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_attendance: failed: '.$e->getMessage());
+ continue;
}
}
}
@@ -3381,228 +4126,437 @@ public function restore_attendance($sessionId = 0)
*
* @param int $sessionId
*/
- public function restore_works($sessionId = 0)
+ public function restore_works(int $sessionId = 0): void
{
- if ($this->course->has_resources(RESOURCE_WORK)) {
- $table = Database::get_course_table(TABLE_STUDENT_PUBLICATION_ASSIGNMENT);
+ if (!$this->course->has_resources(RESOURCE_WORK)) {
+ return;
+ }
- $resources = $this->course->resources;
- foreach ($resources[RESOURCE_WORK] as $obj) {
- // check resources inside html from ckeditor tool and copy correct urls into recipient course
- $obj->params['description'] = DocumentManager::replaceUrlWithNewCourseCode(
- $obj->params['description'],
- $this->course->code,
- $this->course->destination_path,
- $this->course->backup_path,
- $this->course->info['path']
+ $em = Database::getManager();
+ /** @var CourseEntity $courseEntity */
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $sessionId ? api_get_session_entity($sessionId) : null;
+
+ $backupRoot = is_string($this->course->backup_path ?? null) ? rtrim($this->course->backup_path, '/') : '';
+
+ /** @var CStudentPublicationRepository $pubRepo */
+ $pubRepo = Container::getStudentPublicationRepository();
+
+ foreach ($this->course->resources[RESOURCE_WORK] as $legacyId => $obj) {
+ try {
+ $p = (array)($obj->params ?? []);
+
+ $title = trim((string)($p['title'] ?? 'Work'));
+ if ($title === '') { $title = 'Work'; }
+
+ $description = (string)($p['description'] ?? '');
+ if ($description !== '') {
+ $description = ChamiloHelper::rewriteLegacyCourseUrlsToAssets(
+ $description,
+ $courseEntity,
+ $backupRoot
+ ) ?? $description;
+ }
+
+ $enableQualification = (bool)($p['enable_qualification'] ?? false);
+ $addToCalendar = (int)($p['add_to_calendar'] ?? 0) === 1;
+ $expiresOn = !empty($p['expires_on']) ? new \DateTime($p['expires_on']) : null;
+ $endsOn = !empty($p['ends_on']) ? new \DateTime($p['ends_on']) : null;
+
+ $weight = isset($p['weight']) ? (float)$p['weight'] : 0.0;
+ $qualification = isset($p['qualification']) ? (float)$p['qualification'] : 0.0;
+ $allowText = (int)($p['allow_text_assignment'] ?? 0);
+ $defaultVisibility = (bool)($p['default_visibility'] ?? 0);
+ $studentMayDelete = (bool)($p['student_delete_own_publication'] ?? 0);
+ $extensions = isset($p['extensions']) ? (string)$p['extensions'] : null;
+ $groupCategoryWorkId = (int)($p['group_category_work_id'] ?? 0);
+ $postGroupId = (int)($p['post_group_id'] ?? 0);
+
+ $existingQb = $pubRepo->findAllByCourse(
+ $courseEntity,
+ $sessionEntity,
+ $title,
+ null,
+ 'folder'
);
+ $existing = $existingQb
+ ->andWhere('resource.publicationParent IS NULL')
+ ->andWhere('resource.active IN (0,1)')
+ ->setMaxResults(1)
+ ->getQuery()
+ ->getOneOrNullResult();
+
+ if (!$existing) {
+ $pub = new CStudentPublication();
+ $pub->setTitle($title)
+ ->setDescription($description)
+ ->setFiletype('folder')
+ ->setContainsFile(0)
+ ->setWeight($weight)
+ ->setQualification($qualification)
+ ->setAllowTextAssignment($allowText)
+ ->setDefaultVisibility($defaultVisibility)
+ ->setStudentDeleteOwnPublication($studentMayDelete)
+ ->setExtensions($extensions)
+ ->setGroupCategoryWorkId($groupCategoryWorkId)
+ ->setPostGroupId($postGroupId);
+
+ if (method_exists($pub, 'setParent')) {
+ $pub->setParent($courseEntity);
+ }
+ if (method_exists($pub, 'setCreator')) {
+ $pub->setCreator(api_get_user_entity());
+ }
+ if (method_exists($pub, 'addCourseLink')) {
+ $pub->addCourseLink($courseEntity, $sessionEntity);
+ }
- $id_work = $obj->params['id'];
- $obj->params['id'] = null;
- $obj->params['c_id'] = $this->destination_course_info['real_id'];
+ $em->persist($pub);
+ $em->flush();
- // re-create dir
- // @todo check security against injection of dir in crafted course backup here!
- $path = $obj->params['url'];
- $path = '/'.str_replace('/', '', substr($path, 1));
+ // Assignment
+ $assignment = new CStudentPublicationAssignment();
+ $assignment->setPublication($pub)
+ ->setEnableQualification($enableQualification || $qualification > 0);
- $workData = [];
+ if ($expiresOn) { $assignment->setExpiresOn($expiresOn); }
+ if ($endsOn) { $assignment->setEndsOn($endsOn); }
- switch ($this->file_option) {
- case FILE_SKIP:
- $workData = get_work_data_by_path(
- $path,
- $this->destination_course_info['real_id']
- );
- if (!empty($workData)) {
- break;
- }
+ $em->persist($assignment);
+ $em->flush();
- break;
- case FILE_OVERWRITE:
- if (!empty($this->course_origin_id)) {
- $sql = 'SELECT * FROM '.$table.'
- WHERE
- c_id = '.$this->course_origin_id.' AND
- publication_id = '.$id_work;
- $result = Database::query($sql);
- $cant = Database::num_rows($result);
- if ($cant > 0) {
- $row = Database::fetch_assoc($result);
+ // Calendar (URL “Chamilo 2”: Router/UUID)
+ if ($addToCalendar) {
+ $eventTitle = sprintf(get_lang('Handing over of task %s'), $pub->getTitle());
+
+ // URL por UUID o Router
+ $publicationUrl = null;
+ $uuid = $pub->getResourceNode()?->getUuid();
+ if ($uuid) {
+ if (property_exists($this, 'router') && $this->router instanceof RouterInterface) {
+ try {
+ $publicationUrl = $this->router->generate(
+ 'student_publication_view',
+ ['uuid' => (string) $uuid],
+ UrlGeneratorInterface::ABSOLUTE_PATH
+ );
+ } catch (\Throwable) {
+ $publicationUrl = '/r/student_publication/'. $uuid;
+ }
+ } else {
+ $publicationUrl = '/r/student_publication/'. $uuid;
}
-
- $obj->params['enableExpiryDate'] = empty($row['expires_on']) ? false : true;
- $obj->params['enableEndDate'] = empty($row['ends_on']) ? false : true;
- $obj->params['expires_on'] = $row['expires_on'];
- $obj->params['ends_on'] = $row['ends_on'];
- $obj->params['enable_qualification'] = $row['enable_qualification'];
- $obj->params['add_to_calendar'] = !empty($row['add_to_calendar']) ? 1 : 0;
}
- //no break
- case FILE_RENAME:
- $workData = get_work_data_by_path(
- $path,
- $this->destination_course_info['real_id']
+
+ $content = sprintf(
+ '%s
%s',
+ $publicationUrl
+ ? sprintf('%s', $publicationUrl, $pub->getTitle())
+ : htmlspecialchars($pub->getTitle(), ENT_QUOTES),
+ $pub->getDescription()
);
- break;
- }
+ $start = $expiresOn ? clone $expiresOn : new \DateTime('now', new \DateTimeZone('UTC'));
+ $end = $expiresOn ? clone $expiresOn : new \DateTime('now', new \DateTimeZone('UTC'));
- $obj->params['work_title'] = $obj->params['title'];
- $obj->params['new_dir'] = $obj->params['title'];
+ $color = CCalendarEvent::COLOR_STUDENT_PUBLICATION;
+ if ($colors = api_get_setting('agenda.agenda_colors')) {
+ if (!empty($colors['student_publication'])) {
+ $color = $colors['student_publication'];
+ }
+ }
- if (empty($workData)) {
- $workId = addDir(
- $obj->params,
- api_get_user_id(),
- $this->destination_course_info,
- 0,
- $sessionId
- );
- $this->course->resources[RESOURCE_WORK][$id_work]->destination_id = $workId;
+ $event = (new CCalendarEvent())
+ ->setTitle($eventTitle)
+ ->setContent($content)
+ ->setParent($courseEntity)
+ ->setCreator($pub->getCreator())
+ ->addLink(clone $pub->getFirstResourceLink())
+ ->setStartDate($start)
+ ->setEndDate($end)
+ ->setColor($color);
+
+ $em->persist($event);
+ $em->flush();
+
+ $assignment->setEventCalendarId((int)$event->getIid());
+ $em->flush();
+ }
+
+ $this->course->resources[RESOURCE_WORK][$legacyId]->destination_id = (int)$pub->getIid();
} else {
- $workId = $workData['iid'];
- updateWork(
- $workId,
- $obj->params,
- $this->destination_course_info,
- $sessionId
- );
- updatePublicationAssignment(
- $workId,
- $obj->params,
- $this->destination_course_info,
- 0
- );
- $this->course->resources[RESOURCE_WORK][$id_work]->destination_id = $workId;
+ $existing
+ ->setDescription($description)
+ ->setWeight($weight)
+ ->setQualification($qualification)
+ ->setAllowTextAssignment($allowText)
+ ->setDefaultVisibility($defaultVisibility)
+ ->setStudentDeleteOwnPublication($studentMayDelete)
+ ->setExtensions($extensions)
+ ->setGroupCategoryWorkId($groupCategoryWorkId)
+ ->setPostGroupId($postGroupId);
+
+ $em->persist($existing);
+ $em->flush();
+
+ $assignment = $existing->getAssignment();
+ if (!$assignment) {
+ $assignment = new CStudentPublicationAssignment();
+ $assignment->setPublication($existing);
+ $em->persist($assignment);
+ }
+
+ $assignment->setEnableQualification($enableQualification || $qualification > 0);
+ $assignment->setExpiresOn($expiresOn);
+ $assignment->setEndsOn($endsOn);
+ if (!$addToCalendar) {
+ $assignment->setEventCalendarId(0);
+ }
+ $em->flush();
+
+ $this->course->resources[RESOURCE_WORK][$legacyId]->destination_id = (int)$existing->getIid();
}
+ } catch (\Throwable $e) {
+ error_log('COURSE_DEBUG: restore_works: '.$e->getMessage());
+ continue;
}
}
}
- /**
- * Restore gradebook.
- *
- * @param int $sessionId
- *
- * @return bool
- */
- public function restore_gradebook($sessionId = 0)
+
+ public function restore_gradebook(int $sessionId = 0): void
{
- if (in_array($this->file_option, [FILE_SKIP, FILE_RENAME])) {
- return false;
+ if (\in_array($this->file_option, [FILE_SKIP, FILE_RENAME], true)) {
+ return;
}
- // if overwrite
- if ($this->course->has_resources(RESOURCE_GRADEBOOK)) {
- $resources = $this->course->resources;
- $destinationCourseCode = $this->destination_course_info['code'];
- // Delete destination gradebook
- $cats = Category::load(
- null,
- null,
- api_get_course_int_id($destinationCourseCode),
- null,
- null,
- $sessionId
- );
- if (!empty($cats)) {
- /** @var Category $cat */
- foreach ($cats as $cat) {
- $cat->delete_all();
- }
+ if (!$this->course->has_resources(RESOURCE_GRADEBOOK)) {
+ $this->dlog('restore_gradebook: no gradebook resources');
+ return;
+ }
+
+ /** @var EntityManagerInterface $em */
+ $em = \Database::getManager();
+
+ /** @var Course $courseEntity */
+ $courseEntity = api_get_course_entity($this->destination_course_id);
+ /** @var SessionEntity|null $sessionEntity */
+ $sessionEntity = $sessionId ? api_get_session_entity($sessionId) : null;
+ /** @var User $currentUser */
+ $currentUser = api_get_user_entity();
+
+ $catRepo = $em->getRepository(GradebookCategory::class);
+
+ // 1) Clean destination (overwrite semantics)
+ try {
+ $existingCats = $catRepo->findBy([
+ 'course' => $courseEntity,
+ 'session' => $sessionEntity,
+ ]);
+ foreach ($existingCats as $cat) {
+ $em->remove($cat); // cascades remove evaluations/links
}
+ $em->flush();
+ $this->dlog('restore_gradebook: destination cleaned', ['removed' => count($existingCats)]);
+ } catch (\Throwable $e) {
+ $this->dlog('restore_gradebook: clean failed (continuing)', ['error' => $e->getMessage()]);
+ }
- /** @var GradeBookBackup $obj */
- foreach ($resources[RESOURCE_GRADEBOOK] as $id => $obj) {
- if (!empty($obj->categories)) {
- $categoryIdList = [];
- /** @var Category $cat */
- foreach ($obj->categories as $cat) {
- $cat->set_course_code($destinationCourseCode);
- $cat->set_session_id($sessionId);
+ $oldIdToNewCat = [];
+
+ // 2) First pass: create all categories (no parent yet)
+ foreach ($this->course->resources[RESOURCE_GRADEBOOK] as $gbItem) {
+ $categories = (array) ($gbItem->categories ?? []);
+ foreach ($categories as $rawCat) {
+ $c = is_array($rawCat) ? $rawCat : (array) $rawCat;
+
+ $oldId = (int) ($c['id'] ?? $c['iid'] ?? 0);
+ $title = (string)($c['title'] ?? 'Category');
+ $desc = (string)($c['description'] ?? '');
+ $weight = (float) ($c['weight'] ?? 0.0);
+ $visible = (bool) ($c['visible'] ?? true);
+ $locked = (int) ($c['locked'] ?? 0);
+
+ $new = new GradebookCategory();
+ $new->setCourse($courseEntity);
+ $new->setSession($sessionEntity);
+ $new->setUser($currentUser);
+ $new->setTitle($title);
+ $new->setDescription($desc);
+ $new->setWeight($weight);
+ $new->setVisible($visible);
+ $new->setLocked($locked);
+
+ // Optional fields if present in backup
+ if (isset($c['generate_certificates'])) {
+ $new->setGenerateCertificates((bool)$c['generate_certificates']);
+ } elseif (isset($c['generateCertificates'])) {
+ $new->setGenerateCertificates((bool)$c['generateCertificates']);
+ }
+ if (isset($c['certificate_validity_period'])) {
+ $new->setCertificateValidityPeriod((int)$c['certificate_validity_period']);
+ } elseif (isset($c['certificateValidityPeriod'])) {
+ $new->setCertificateValidityPeriod((int)$c['certificateValidityPeriod']);
+ }
+ if (isset($c['is_requirement'])) {
+ $new->setIsRequirement((bool)$c['is_requirement']);
+ } elseif (isset($c['isRequirement'])) {
+ $new->setIsRequirement((bool)$c['isRequirement']);
+ }
+ if (isset($c['default_lowest_eval_exclude'])) {
+ $new->setDefaultLowestEvalExclude((bool)$c['default_lowest_eval_exclude']);
+ } elseif (isset($c['defaultLowestEvalExclude'])) {
+ $new->setDefaultLowestEvalExclude((bool)$c['defaultLowestEvalExclude']);
+ }
+ if (array_key_exists('minimum_to_validate', $c)) {
+ $new->setMinimumToValidate((int)$c['minimum_to_validate']);
+ } elseif (array_key_exists('minimumToValidate', $c)) {
+ $new->setMinimumToValidate((int)$c['minimumToValidate']);
+ }
+ if (array_key_exists('gradebooks_to_validate_in_dependence', $c)) {
+ $new->setGradeBooksToValidateInDependence((int)$c['gradebooks_to_validate_in_dependence']);
+ } elseif (array_key_exists('gradeBooksToValidateInDependence', $c)) {
+ $new->setGradeBooksToValidateInDependence((int)$c['gradeBooksToValidateInDependence']);
+ }
+ if (array_key_exists('allow_skills_by_subcategory', $c)) {
+ $new->setAllowSkillsBySubcategory((int)$c['allow_skills_by_subcategory']);
+ } elseif (array_key_exists('allowSkillsBySubcategory', $c)) {
+ $new->setAllowSkillsBySubcategory((int)$c['allowSkillsBySubcategory']);
+ }
+ if (!empty($c['grade_model_id'])) {
+ $gm = $em->find(GradeModel::class, (int)$c['grade_model_id']);
+ if ($gm) { $new->setGradeModel($gm); }
+ }
- $parentId = $cat->get_parent_id();
- if (!empty($parentId)) {
- if (isset($categoryIdList[$parentId])) {
- $cat->set_parent_id($categoryIdList[$parentId]);
- }
- }
- $oldId = $cat->get_id();
- $categoryId = $cat->add();
- $categoryIdList[$oldId] = $categoryId;
- if (!empty($cat->evaluations)) {
- /** @var Evaluation $evaluation */
- foreach ($cat->evaluations as $evaluation) {
- $evaluation->set_category_id($categoryId);
- $evaluation->set_course_code($destinationCourseCode);
- $evaluation->setSessionId($sessionId);
- $evaluation->add();
- }
- }
+ $em->persist($new);
+ $em->flush();
- if (!empty($cat->links)) {
- /** @var AbstractLink $link */
- foreach ($cat->links as $link) {
- $link->set_category_id($categoryId);
- $link->set_course_code($destinationCourseCode);
- $link->set_session_id($sessionId);
- $import = false;
- $itemId = $link->get_ref_id();
- switch ($link->get_type()) {
- case LINK_EXERCISE:
- $type = RESOURCE_QUIZ;
+ if ($oldId > 0) {
+ $oldIdToNewCat[$oldId] = $new;
+ }
+ }
+ }
- break;
- /*case LINK_DROPBOX:
- break;*/
- case LINK_STUDENTPUBLICATION:
- $type = RESOURCE_WORK;
+ // 3) Second pass: wire parents
+ foreach ($this->course->resources[RESOURCE_GRADEBOOK] as $gbItem) {
+ $categories = (array) ($gbItem->categories ?? []);
+ foreach ($categories as $rawCat) {
+ $c = is_array($rawCat) ? $rawCat : (array) $rawCat;
+ $oldId = (int)($c['id'] ?? $c['iid'] ?? 0);
+ $parentOld = (int)($c['parent_id'] ?? $c['parentId'] ?? 0);
+ if ($oldId > 0 && isset($oldIdToNewCat[$oldId]) && $parentOld > 0 && isset($oldIdToNewCat[$parentOld])) {
+ $cat = $oldIdToNewCat[$oldId];
+ $cat->setParent($oldIdToNewCat[$parentOld]);
+ $em->persist($cat);
+ }
+ }
+ }
+ $em->flush();
+
+ // 4) Evaluations + Links
+ foreach ($this->course->resources[RESOURCE_GRADEBOOK] as $gbItem) {
+ $categories = (array) ($gbItem->categories ?? []);
+ foreach ($categories as $rawCat) {
+ $c = is_array($rawCat) ? $rawCat : (array) $rawCat;
+ $oldId = (int)($c['id'] ?? $c['iid'] ?? 0);
+ if ($oldId <= 0 || !isset($oldIdToNewCat[$oldId])) { continue; }
+
+ $dstCat = $oldIdToNewCat[$oldId];
+
+ // Evaluations
+ foreach ((array)($c['evaluations'] ?? []) as $rawEval) {
+ $e = is_array($rawEval) ? $rawEval : (array) $rawEval;
+
+ $eval = new GradebookEvaluation();
+ $eval->setCourse($courseEntity);
+ $eval->setCategory($dstCat);
+ $eval->setTitle((string)($e['title'] ?? 'Evaluation'));
+ $eval->setDescription((string)($e['description'] ?? ''));
+ $eval->setWeight((float)($e['weight'] ?? 0.0));
+ $eval->setMax((float)($e['max'] ?? 100.0));
+ $eval->setType((string)($e['type'] ?? 'manual'));
+ $eval->setVisible((int)($e['visible'] ?? 1));
+ $eval->setLocked((int)($e['locked'] ?? 0));
+
+ if (isset($e['best_score'])) { $eval->setBestScore((float)$e['best_score']); }
+ if (isset($e['average_score'])) { $eval->setAverageScore((float)$e['average_score']); }
+ if (isset($e['score_weight'])) { $eval->setScoreWeight((float)$e['score_weight']); }
+ if (isset($e['min_score'])) { $eval->setMinScore((float)$e['min_score']); }
+
+ $em->persist($eval);
+ }
- break;
- case LINK_LEARNPATH:
- $type = RESOURCE_LEARNPATH;
+ // Links
+ foreach ((array)($c['links'] ?? []) as $rawLink) {
+ $l = is_array($rawLink) ? $rawLink : (array) $rawLink;
- break;
- case LINK_FORUM_THREAD:
- $type = RESOURCE_FORUMTOPIC;
+ $linkType = (int)($l['type'] ?? $l['link_type'] ?? 0);
+ $legacyRef = (int)($l['ref_id'] ?? $l['refId'] ?? 0);
+ if ($linkType <= 0 || $legacyRef <= 0) {
+ $this->dlog('restore_gradebook: skipping link (missing type/ref)', $l);
+ continue;
+ }
- break;
- case LINK_ATTENDANCE:
- $type = RESOURCE_ATTENDANCE;
+ $resourceType = $this->gb_guessResourceTypeByLinkType($linkType);
+ $newRefId = $this->gb_resolveDestinationId($resourceType, $legacyRef);
+ if ($newRefId <= 0) {
+ $this->dlog('restore_gradebook: skipping link (no destination id)', ['type' => $linkType, 'legacyRef' => $legacyRef]);
+ continue;
+ }
- break;
- case LINK_SURVEY:
- $type = RESOURCE_ATTENDANCE;
+ $link = new GradebookLink();
+ $link->setCourse($courseEntity);
+ $link->setCategory($dstCat);
+ $link->setType($linkType);
+ $link->setRefId($newRefId);
+ $link->setWeight((float)($l['weight'] ?? 0.0));
+ $link->setVisible((int)($l['visible'] ?? 1));
+ $link->setLocked((int)($l['locked'] ?? 0));
+
+ if (isset($l['best_score'])) { $link->setBestScore((float)$l['best_score']); }
+ if (isset($l['average_score'])) { $link->setAverageScore((float)$l['average_score']); }
+ if (isset($l['score_weight'])) { $link->setScoreWeight((float)$l['score_weight']); }
+ if (isset($l['min_score'])) { $link->setMinScore((float)$l['min_score']); }
+
+ $em->persist($link);
+ }
- break;
- case LINK_HOTPOTATOES:
- $type = RESOURCE_QUIZ;
+ $em->flush();
+ }
+ }
- break;
- }
+ $this->dlog('restore_gradebook: done');
+ }
- if ($this->course->has_resources($type) &&
- isset($this->course->resources[$type][$itemId])
- ) {
- $item = $this->course->resources[$type][$itemId];
- if ($item && $item->is_restored()) {
- $link->set_ref_id($item->destination_id);
- $import = true;
- }
- }
+ /** Map GradebookLink type → RESOURCE_* bucket used in $this->course->resources */
+ private function gb_guessResourceTypeByLinkType(int $linkType): ?int
+ {
+ return match ($linkType) {
+ LINK_EXERCISE => RESOURCE_QUIZ,
+ LINK_STUDENTPUBLICATION => RESOURCE_WORK,
+ LINK_LEARNPATH => RESOURCE_LEARNPATH,
+ LINK_FORUM_THREAD => RESOURCE_FORUMTOPIC,
+ LINK_ATTENDANCE => RESOURCE_ATTENDANCE,
+ LINK_SURVEY => RESOURCE_SURVEY,
+ LINK_HOTPOTATOES => RESOURCE_QUIZ,
+ default => null,
+ };
+ }
- if ($import) {
- $link->add();
- }
- }
- }
- }
- }
- }
- }
+ /** Given a RESOURCE_* bucket and legacy id, return destination id (if that item was restored) */
+ private function gb_resolveDestinationId(?int $type, int $legacyId): int
+ {
+ if (null === $type) { return 0; }
+ if (!$this->course->has_resources($type)) { return 0; }
+ $bucket = $this->course->resources[$type] ?? [];
+ if (!isset($bucket[$legacyId])) { return 0; }
+ $res = $bucket[$legacyId];
+ $destId = (int)($res->destination_id ?? 0);
+ return $destId > 0 ? $destId : 0;
}
+
/**
* Restore course assets (not included in documents).
*/
@@ -3633,109 +4587,4 @@ public function restore_assets()
}
}
}
-
- /**
- * @param string $str
- *
- * @return string
- */
- public function DBUTF8($str)
- {
- if (UTF8_CONVERT) {
- $str = utf8_encode($str);
- }
-
- return $str;
- }
-
- /**
- * @param string $str
- *
- * @return string
- */
- public function DBUTF8escapestring($str)
- {
- if (UTF8_CONVERT) {
- $str = utf8_encode($str);
- }
-
- return Database::escape_string($str);
- }
-
- /**
- * @param array $array
- */
- public function DBUTF8_array($array)
- {
- if (UTF8_CONVERT) {
- foreach ($array as &$item) {
- $item = utf8_encode($item);
- }
-
- return $array;
- } else {
- return $array;
- }
- }
-
- /**
- * @param int $groupId
- *
- * @return array
- */
- public function checkGroupId($groupId)
- {
- return GroupManager::get_group_properties($groupId);
- }
-
- /**
- * @param string $documentPath
- * @param string $webEditorCss
- */
- public function fixEditorHtmlContent($documentPath, $webEditorCss = '')
- {
- $extension = pathinfo(basename($documentPath), PATHINFO_EXTENSION);
-
- switch ($extension) {
- case 'html':
- case 'htm':
- $contents = file_get_contents($documentPath);
- $contents = str_replace(
- '{{css_editor}}',
- $webEditorCss,
- $contents
- );
- file_put_contents($documentPath, $contents);
-
- break;
- }
- }
-
- /**
- * Check if user exist otherwise use current user.
- *
- * @param int $userId
- * @param bool $returnNull
- *
- * @return int
- */
- private function checkUserId($userId, $returnNull = false)
- {
- if (!empty($userId)) {
- $userInfo = api_get_user_info($userId);
- if (empty($userInfo)) {
- return api_get_user_id();
- }
- }
-
- if ($returnNull) {
- return null;
- }
-
- if (empty($userId)) {
- return api_get_user_id();
- }
-
- return $userId;
- }
}
diff --git a/src/CourseBundle/Component/CourseCopy/Resources/Document.php b/src/CourseBundle/Component/CourseCopy/Resources/Document.php
index b40de2b3a5c..088ec41560c 100644
--- a/src/CourseBundle/Component/CourseCopy/Resources/Document.php
+++ b/src/CourseBundle/Component/CourseCopy/Resources/Document.php
@@ -1,53 +1,33 @@
- */
class Document extends Resource
{
- public $path;
- public $comment;
- public $file_type;
- public $size;
- public $title;
+ public string $path;
+ public ?string $comment = null;
+ public string $file_type;
+ public string $size;
+ public string $title;
- /**
- * Create a new Document.
- *
- * @param int $id
- * @param string $path
- * @param string $comment
- * @param string $title
- * @param string $file_type (DOCUMENT or FOLDER);
- * @param int $size
- */
- public function __construct($id, $path, $comment, $title, $file_type, $size)
+ public function __construct($id, $fullPath, $comment, $title, $file_type, $size)
{
parent::__construct($id, RESOURCE_DOCUMENT);
- $this->path = 'document'.$path;
- $this->comment = $comment;
- $this->title = $title;
- $this->file_type = $file_type;
- $this->size = $size;
+ $clean = ltrim((string)$fullPath, '/');
+ $this->path = 'document/'.$clean;
+ $this->comment = $comment ?? '';
+ $this->title = (string)$title;
+ $this->file_type = (string)$file_type;
+ $this->size = (string)$size;
}
- /**
- * Show this document.
- */
public function show()
{
parent::show();
echo preg_replace('@^document@', '', $this->path);
- if (!empty($this->title)) {
- if (false === strpos($this->path, $this->title)) {
- echo ' - '.$this->title;
- }
+ if (!empty($this->title) && false === strpos($this->path, $this->title)) {
+ echo ' - '.$this->title;
}
}
}
diff --git a/src/CourseBundle/Component/CourseCopy/Resources/ForumCategory.php b/src/CourseBundle/Component/CourseCopy/Resources/ForumCategory.php
index 34638dad6c7..4fb6edaf321 100644
--- a/src/CourseBundle/Component/CourseCopy/Resources/ForumCategory.php
+++ b/src/CourseBundle/Component/CourseCopy/Resources/ForumCategory.php
@@ -4,28 +4,23 @@
namespace Chamilo\CourseBundle\Component\CourseCopy\Resources;
-/**
- * A forum-category.
- *
- * @author Bart Mollet
- */
class ForumCategory extends Resource
{
- /**
- * Create a new ForumCategory.
- */
+ public ?string $title = null;
+ public ?string $description = null;
+
public function __construct($obj)
{
parent::__construct($obj->cat_id, RESOURCE_FORUMCATEGORY);
$this->obj = $obj;
+
+ $this->title = (string) ($obj->cat_title ?? $obj->title ?? '');
+ $this->description = (string) ($obj->cat_comment ?? $obj->description ?? '');
}
- /**
- * Show this resource.
- */
public function show()
{
parent::show();
- echo $this->obj->title;
+ echo $this->obj->cat_title ?? $this->obj->title ?? '';
}
}
diff --git a/src/CourseBundle/Component/CourseCopy/Resources/ForumPost.php b/src/CourseBundle/Component/CourseCopy/Resources/ForumPost.php
index 5a5635adb79..fd303963705 100644
--- a/src/CourseBundle/Component/CourseCopy/Resources/ForumPost.php
+++ b/src/CourseBundle/Component/CourseCopy/Resources/ForumPost.php
@@ -4,28 +4,34 @@
namespace Chamilo\CourseBundle\Component\CourseCopy\Resources;
-/**
- * A forum-post.
- *
- * @author Bart Mollet
- */
class ForumPost extends Resource
{
- /**
- * Create a new ForumPost.
- */
+ public ?string $title = null;
+ public ?string $text = null;
+ public ?string $poster_name = null;
+
public function __construct($obj)
{
parent::__construct($obj->post_id, RESOURCE_FORUMPOST);
$this->obj = $obj;
+
+ $this->title = (string)($obj->post_title ?? $obj->title ?? '');
+ $this->text = (string)($obj->post_text ?? $obj->text ?? '');
+ $this->poster_name = (string)($obj->poster_name ?? '');
}
- /**
- * Show this resource.
- */
public function show()
{
parent::show();
- echo $this->obj->title.' ('.$this->obj->poster_name.', '.$this->obj->post_date.')';
+
+ $date = $this->obj->post_date ?? ($this->obj->time ?? null);
+ $dateStr = $date ? api_convert_and_format_date($date) : '';
+
+ $extra = $this->poster_name ? $this->poster_name : '';
+ if ($dateStr) {
+ $extra = $extra ? ($extra.', '.$dateStr) : $dateStr;
+ }
+
+ echo $this->title.($extra ? ' ('.$extra.')' : '');
}
}
diff --git a/src/CourseBundle/Component/CourseCopy/Resources/ForumTopic.php b/src/CourseBundle/Component/CourseCopy/Resources/ForumTopic.php
index c1c726e9b19..9dd98382b06 100644
--- a/src/CourseBundle/Component/CourseCopy/Resources/ForumTopic.php
+++ b/src/CourseBundle/Component/CourseCopy/Resources/ForumTopic.php
@@ -4,50 +4,37 @@
namespace Chamilo\CourseBundle\Component\CourseCopy\Resources;
-/**
- * A forum-topic/thread.
- *
- * @author Bart Mollet
- */
class ForumTopic extends Resource
{
- /**
- * Create a new ForumTopic.
- */
- /* function ForumTopic($id, $title, $time, $topic_poster_id, $topic_poster_name, $forum_id, $last_post, $replies, $views = 0, $sticky = 0, $locked = 0,
- $time_closed = null, $weight = 0, $title_qualify = null, $qualify_max = 0) */
+ public ?string $title = null;
+ public ?string $topic_poster_name = null;
+ public ?string $title_qualify = null;
+
public function __construct($obj)
{
parent::__construct($obj->thread_id, RESOURCE_FORUMTOPIC);
$this->obj = $obj;
- /*
- $this->title = $title;
- $this->time = $time;
- $this->topic_poster_id = $topic_poster_id;
- $this->topic_poster_name = $topic_poster_name;
- $this->forum_id = $forum_id;
- $this->last_post = $last_post;
- $this->replies = $replies;
- $this->views = $views;
- $this->sticky = $sticky;
- $this->locked = $locked;
- $this->time_closed = $time_closed;
- $this->weight = $weight;
- $this->title_qualify = $title_qualify;
- $this->qualify_max = $qualify_max; */
+
+ $this->title = (string)($obj->thread_title ?? $obj->title ?? '');
+ $this->topic_poster_name = (string)($obj->thread_poster_name ?? $obj->topic_poster_name ?? '');
+ $this->title_qualify = (string)($obj->thread_title_qualify ?? $obj->title_qualify ?? '');
}
- /**
- * Show this resource.
- */
public function show()
{
parent::show();
- $extra = api_convert_and_format_date($this->obj->thread_date);
- if ($this->obj->thread_poster_id) {
- $user_info = api_get_user_info($this->obj->thread_poster_id);
- $extra = $user_info['complete_name'].', '.$extra;
+
+ $date = $this->obj->thread_date ?? ($this->obj->time ?? null);
+ $extra = $date ? api_convert_and_format_date($date) : '';
+
+ if (!empty($this->obj->thread_poster_id)) {
+ $ui = api_get_user_info($this->obj->thread_poster_id);
+ $name = $ui['complete_name'] ?? $this->topic_poster_name;
+ $extra = ($name ? $name.', ' : '').$extra;
+ } elseif (!empty($this->topic_poster_name)) {
+ $extra = $this->topic_poster_name.', '.$extra;
}
- echo $this->obj->title.' ('.$extra.')';
+
+ echo $this->title.($this->title_qualify ? ' ['.$this->title_qualify.']' : '').($extra ? ' ('.$extra.')' : '');
}
}
diff --git a/src/CourseBundle/Component/CourseCopy/Resources/Work.php b/src/CourseBundle/Component/CourseCopy/Resources/Work.php
index 7148d4b5b7e..1ac00b5f3ec 100644
--- a/src/CourseBundle/Component/CourseCopy/Resources/Work.php
+++ b/src/CourseBundle/Component/CourseCopy/Resources/Work.php
@@ -1,32 +1,46 @@
+ * Work/Assignment/Student publication backup resource wrapper.
*/
class Work extends Resource
{
- public $params = [];
+ /** Raw backup parameters (id, title, description, url, etc.). */
+ public array $params = [];
- /**
- * Create a new Work.
- *
- * @param array $params
- */
- public function __construct($params)
+ /** Plain properties used by legacy restorer helpers (e.g. to_system_encoding). */
+ public string $title = '';
+ public string $description = '';
+ public ?string $url = null;
+
+ public function __construct(array $params)
{
- parent::__construct($params['id'], RESOURCE_WORK);
- $this->params = $params;
+ parent::__construct((int)($params['id'] ?? 0), RESOURCE_WORK);
+
+ $this->params = $params;
+ $this->title = isset($params['title']) ? (string) $params['title'] : '';
+ $this->description = isset($params['description']) ? (string) $params['description'] : '';
+ $this->url = isset($params['url']) && is_string($params['url']) ? $params['url'] : null;
}
- public function show()
+ public function show(): void
{
parent::show();
- echo $this->params['title'];
+ echo htmlspecialchars($this->title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ }
+
+ /**
+ * Convenience accessor for the backup path, if you prefer not to read $url directly.
+ */
+ public function getBackupPath(): ?string
+ {
+ return $this->url
+ ?? (isset($this->params['url']) && is_string($this->params['url']) ? $this->params['url'] : null);
}
}
diff --git a/src/CourseBundle/Entity/CQuizQuestionOption.php b/src/CourseBundle/Entity/CQuizQuestionOption.php
index 8e64ba71db1..b553bddc28c 100644
--- a/src/CourseBundle/Entity/CQuizQuestionOption.php
+++ b/src/CourseBundle/Entity/CQuizQuestionOption.php
@@ -39,6 +39,11 @@ public function setTitle(string $title): self
return $this;
}
+ public function getIid()
+ {
+ return $this->iid;
+ }
+
/**
* Get name.
*
diff --git a/src/CourseBundle/Entity/CThematicAdvance.php b/src/CourseBundle/Entity/CThematicAdvance.php
index d1c2a2a4568..e921817e3b0 100644
--- a/src/CourseBundle/Entity/CThematicAdvance.php
+++ b/src/CourseBundle/Entity/CThematicAdvance.php
@@ -28,7 +28,7 @@ class CThematicAdvance implements Stringable // extends AbstractResource impleme
#[ORM\ManyToOne(targetEntity: CAttendance::class)]
#[ORM\JoinColumn(name: 'attendance_id', referencedColumnName: 'iid', onDelete: 'CASCADE')]
- protected CAttendance $attendance;
+ protected ?CAttendance $attendance = null;
#[ORM\Column(name: 'content', type: 'text', nullable: true)]
protected ?string $content = null;