diff --git a/composer.json b/composer.json deleted file mode 100755 index 486f75734f6..00000000000 --- a/composer.json +++ /dev/null @@ -1,202 +0,0 @@ -{ - "name": "chamilo/chamilo-lms", - "description": "E-learning and collaboration software", - "type": "project", - "homepage": "http://www.chamilo.org", - "license": "GPL-3.0", - "support": { - "docs": "https://docs.chamilo.org/", - "forum": "https://forum.chamilo.org/", - "issues": "https://github.com/chamilo/chamilo-lms/issues", - "source": "https://github.com/chamilo/chamilo-lms" - }, - "autoload": { - "psr-4": { - "Application\\": "app/", - "Chamilo\\": "src/Chamilo/" - }, - "classmap": [ - "main/admin", - "main/auth", - "main/course_description", - "main/cron/lang", - "main/dropbox", - "main/exercise", - "main/gradebook/lib", - "main/inc/lib", - "main/inc/lib/hook", - "main/install", - "main/lp", - "main/survey", - "main/common_cartridge/export", - "main/common_cartridge/import", - "plugin" - ] - }, - "require": { - "php": "^7.4", - "ext-curl": "*", - "ext-dom": "*", - "ext-fileinfo": "*", - "ext-gd": "*", - "ext-intl": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-zip": "*", - "ext-zlib": "*", - "angelfqc/vimeo-api": "2.0.6", - "apereo/phpcas": "^1.6", - "chamilo/pclzip": "~2.8", - "clue/graph": "~0.9.0", - "culqi/culqi-php": "1.3.4", - "ddeboer/data-import": "@stable", - "doctrine/data-fixtures": "~1.0@dev", - "doctrine/dbal": "~2.5", - "doctrine/migrations": "~1.0@dev", - "doctrine/orm": "~2.5", - "emojione/emojione": "1.3.0", - "endroid/qr-code": "2.5.*", - "enshrined/svg-sanitize": "^0.16.0", - "essence/essence": "2.6.1", - "ezyang/htmlpurifier": "~4.9", - "facebook/graph-sdk": "^5.7", - "firebase/php-jwt": "~5.0", - "gedmo/doctrine-extensions": "~2.3", - "graphp/algorithms": "~0.8.0", - "graphp/graphviz": "~0.2.0", - "guzzlehttp/guzzle": "~6.0", - "h5p/h5p-core": "*", - "imagine/imagine": "0.6.3", - "ircmaxell/password-compat": "~1.0.4", - "jbroadway/urlify": "1.1.0-stable", - "jeroendesloovere/vcard": "~1.7", - "jimmiw/php-time-ago": "0.4.15", - "kigkonsult/icalcreator": "2.24", - "knplabs/doctrine-behaviors": "~1.1", - "knplabs/gaufrette": "~0.3", - "knplabs/knp-components": "~1.3", - "league/csv": "~8.0", - "media-alchemyst/media-alchemyst": "~0.5", - "michelf/php-markdown": "~1.7", - "monolog/monolog": "~1.0", - "mpdf/mpdf": "^8.0", - "ocramius/proxy-manager": "~1.0|2.0.*", - "onelogin/php-saml": "^3.0", - "paragonie/random-lib": "2.0.0", - "patchwork/utf8": "~1.2", - "php-ffmpeg/php-ffmpeg": "0.5.1", - "php-http/guzzle6-adapter": "^2.0", - "php-xapi/client": "0.7.x-dev", - "php-xapi/repository-api": "dev-master as 0.3.1", - "php-xapi/repository-doctrine": "dev-master", - "php-xapi/symfony-serializer": "2.1.0 as 2.0", - "phpmailer/phpmailer": "~6.1", - "phpoffice/phpexcel": "~1.8", - "phpoffice/phpword": "~0.14", - "phpseclib/phpseclib": "^2.0", - "robrichards/xmlseclibs": "3.0.*", - "sabre/vobject": "~3.1", - "sonata-project/admin-bundle": "~3.1|~4.0", - "sonata-project/core-bundle": "~3.1|~4.0", - "sonata-project/user-bundle": "~3.0|~4.0", - "stripe/stripe-php": "*", - "studio-42/elfinder": "2.1.*", - "sunra/php-simple-html-dom-parser": "~1.5.0", - "sylius/attribute": "0.13.0", - "sylius/translation": "0.13.0", - "symfony/console": "~3.0|~4.0", - "symfony/doctrine-bridge": "~2.8", - "symfony/dom-crawler": "~3.4|~4.0", - "symfony/filesystem": "~3.0|~4.0", - "symfony/http-foundation": "~2.8|~3.0", - "symfony/security": "~3.0|~4.0", - "symfony/serializer": "~3.0|~4.0", - "symfony/validator": "~3.0|~4.0", - "symfony/yaml": "~3.0|~4.0", - "szymach/c-pchart": "~3.0", - "thenetworg/oauth2-azure": "^1.4", - "twig/extensions": "~1.0", - "twig/twig": "1.*", - "webit/eval-math": "1.0.1", - "yuloh/bccomp-polyfill": "dev-master", - "packbackbooks/lti-1p3-tool": "1.1.1.x-dev", - "zendframework/zend-config": "~3.0", - "zendframework/zend-feed": "~2.6|^3.0", - "zendframework/zend-http": "~2.6|^3.0", - "zendframework/zend-soap": "~2.6|^3.0" - }, - "require-dev": { - "behat/behat": "~3.5", - "behat/mink": "1.7.1", - "behat/mink-extension": "*", - "behat/mink-goutte-driver": "*", - "behat/mink-selenium2-driver": "*", - "phpunit/phpunit": "*" - }, - "scripts": { - "pre-install-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x" - ], - "pre-update-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x" - ], - "post-install-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::dumpCssFiles", - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::generateDoctrineProxies" - ], - "post-update-cmd": [ - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::dumpCssFiles", - "Chamilo\\CoreBundle\\Composer\\ScriptHandler::generateDoctrineProxies" - ], - "update-css": "Chamilo\\CoreBundle\\Composer\\ScriptHandler::updateCss" - }, - "extra": { - "asset-installer-paths": { - "bower-asset-library": "web/assets/" - }, - "branch-alias": { - "dev-master": "1.11.x-dev" - }, - "incenteev-parameters": { - "file": "app/config/parameters.yml" - }, - "symfony-app-dir": "app", - "symfony-assets-install": "relative", - "symfony-bin-dir": "bin", - "symfony-tests-dir": "tests", - "symfony-web-dir": "web" - }, - "repositories": [ - { - "type": "github", - "url": "https://github.com/AngelFQC/vimeo.php.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/AngelFQC/xapi-model.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/AngelFQC/xapi-repository-doctrine.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/AngelFQC/xapi-symfony-serializer.git", - "no-api": true - }, - { - "type": "github", - "url": "https://github.com/chamilo/lti-1-3-php-library.git", - "no-api": true - } - ], - "config": { - "sort-packages": true, - "component-dir": "web/assets" - } -} diff --git a/main/inc/lib/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php index e7cd8951326..f7d66519f78 100644 --- a/main/inc/lib/moodleexport/ActivityExport.php +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -14,6 +14,7 @@ abstract class ActivityExport { protected $course; + public const DOCS_MODULE_ID = 1000000; public function __construct($course) { @@ -27,15 +28,44 @@ public function __construct($course) abstract public function export($activityId, $exportDir, $moduleId, $sectionId); /** - * Get the section ID for a given activity ID. + * Get the section ID (learnpath source_id) for a given activity. */ public function getSectionIdForActivity(int $activityId, string $itemType): int { + if (empty($this->course->resources[RESOURCE_LEARNPATH])) { + return 0; + } + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { + if (empty($learnpath->items)) { + continue; + } + foreach ($learnpath->items as $item) { - $item['item_type'] = $item['item_type'] === 'student_publication' ? 'work' : $item['item_type']; - if ($item['item_type'] == $itemType && $item['path'] == $activityId) { - return $learnpath->source_id; + $normalizedType = $item['item_type'] === 'student_publication' + ? 'work' + : $item['item_type']; + + if ($normalizedType !== $itemType) { + continue; + } + + // Classic case: LP stores the numeric id in "path" + if (ctype_digit((string) $item['path']) && (int) $item['path'] === $activityId) { + return (int) $learnpath->source_id; + } + + // Fallback for documents when LP stores the path instead of the id + if ($itemType === RESOURCE_DOCUMENT) { + $doc = \DocumentManager::get_document_data_by_id($activityId, $this->course->code); + if (!empty($doc['path'])) { + $p = (string) $doc['path']; + foreach ([$p, 'document/'.$p, '/'.$p] as $candidate) { + if ((string) $item['path'] === $candidate) { + return (int) $learnpath->source_id; + } + } + } } } } @@ -252,4 +282,26 @@ protected function createCalendarXml(array $activityData, string $destinationDir $this->createXmlFile('calendar', $xmlContent, $destinationDir); } + + /** + * Returns the title of the item in the LP (if it exists); otherwise, $fallback. + */ + protected function lpItemTitle(int $sectionId, string $itemType, int $resourceId, ?string $fallback): string + { + if (!isset($this->course->resources[RESOURCE_LEARNPATH])) { + return $fallback ?? ''; + } + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { + if ((int) $lp->source_id !== $sectionId || empty($lp->items)) { + continue; + } + foreach ($lp->items as $it) { + $type = $it['item_type'] === 'student_publication' ? 'work' : $it['item_type']; + if ($type === $itemType && (int) $it['path'] === $resourceId) { + return $it['title'] ?? ($fallback ?? ''); + } + } + } + return $fallback ?? ''; + } } diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php index 059e667dc48..d42fbf2d889 100644 --- a/main/inc/lib/moodleexport/FileExport.php +++ b/main/inc/lib/moodleexport/FileExport.php @@ -197,37 +197,28 @@ private function createFileXmlEntry(array $file): string */ private function processDocument(array $filesData, object $document): array { - if ( - $document->file_type === 'file' && - isset($this->course->used_page_doc_ids) && - in_array($document->source_id, $this->course->used_page_doc_ids) - ) { + // Only real files are exported; folders are represented implicitly by "filepath" + if ($document->file_type !== 'file') { return $filesData; } - if ( - $document->file_type === 'file' && - pathinfo($document->path, PATHINFO_EXTENSION) === 'html' && - substr_count($document->path, '/') === 1 - ) { - return $filesData; - } + // Base file data (contenthash, size, title, etc.) + $fileData = $this->getFileData($document); - if ($document->file_type === 'file') { - $extension = pathinfo($document->path, PATHINFO_EXTENSION); - if (!in_array(strtolower($extension), ['html', 'htm'])) { - $fileData = $this->getFileData($document); - $fileData['filepath'] = '/Documents/'; - $fileData['contextid'] = 0; - $fileData['component'] = 'mod_folder'; - $filesData['files'][] = $fileData; - } - } elseif ($document->file_type === 'folder') { - $folderFiles = \DocumentManager::getAllDocumentsByParentId($this->course->info, $document->source_id); - foreach ($folderFiles as $file) { - $filesData['files'][] = $this->getFolderFileData($file, (int) $document->source_id, '/Documents/'.dirname($file['path']).'/'); - } - } + // Rebuild the relative filepath so Moodle can recreate the Documents tree + $relDir = dirname($document->path); + $filepath = $this->ensureTrailingSlash( + $relDir === '.' ? '/' : '/'.$relDir.'/' + ); + + // Attach this file to the global "Documents" folder activity + $fileData['filepath'] = $filepath; + $fileData['contextid'] = ActivityExport::DOCS_MODULE_ID; + $fileData['component'] = 'mod_folder'; + $fileData['filearea'] = 'content'; + $fileData['itemid'] = ActivityExport::DOCS_MODULE_ID; + + $filesData['files'][] = $fileData; return $filesData; } @@ -267,22 +258,23 @@ private function getFileData(object $document): array /** * Get file data for files inside a folder. */ - private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/Documents/'): array + private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/'): array { $adminData = MoodleExport::getAdminUserData(); $adminId = $adminData['id']; $contenthash = hash('sha1', basename($file['path'])); $mimetype = $this->getMimeType($file['path']); $filename = basename($file['path']); - $filepath = $this->ensureTrailingSlash($parentPath); + $relDir = dirname($file['path']); + $filepath = $this->ensureTrailingSlash($relDir === '.' ? '/' : '/'.$relDir.'/'); return [ 'id' => $file['id'], 'contenthash' => $contenthash, - 'contextid' => $sourceId, + 'contextid' => ActivityExport::DOCS_MODULE_ID, 'component' => 'mod_folder', 'filearea' => 'content', - 'itemid' => (int) $file['id'], + 'itemid' => ActivityExport::DOCS_MODULE_ID, 'filepath' => $filepath, 'documentpath' => 'document/'.$file['path'], 'filename' => $filename, diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php index a1b1ac6dd0f..9cd9f3be7e8 100644 --- a/main/inc/lib/moodleexport/FolderExport.php +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -44,12 +44,12 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void */ public function getData(int $folderId, int $sectionId): ?array { - if ($folderId === 0) { + if ($folderId === 0 || $folderId === ActivityExport::DOCS_MODULE_ID) { return [ - 'id' => 0, - 'moduleid' => 0, + 'id' => ActivityExport::DOCS_MODULE_ID, + 'moduleid' => ActivityExport::DOCS_MODULE_ID, 'modulename' => 'folder', - 'contextid' => 0, + 'contextid' => ActivityExport::DOCS_MODULE_ID, 'name' => 'Documents', 'sectionid' => $sectionId, 'timemodified' => time(), @@ -98,17 +98,10 @@ private function createFolderXml(array $folderData, string $folderDir): void private function getFilesForFolder(int $folderId): array { $files = []; - if ($folderId === 0) { + if ($folderId === ActivityExport::DOCS_MODULE_ID) { foreach ($this->course->resources[RESOURCE_DOCUMENT] as $doc) { if ($doc->file_type === 'file') { - $files[] = [ - 'id' => (int) $doc->source_id, - 'contenthash' => hash('sha1', basename($doc->path)), - 'filename' => basename($doc->path), - 'filepath' => '/Documents/', - 'filesize' => (int) $doc->size, - 'mimetype' => $this->getMimeType($doc->path), - ]; + $files[] = ['id' => (int) $doc->source_id]; } } } diff --git a/main/inc/lib/moodleexport/ForumExport.php b/main/inc/lib/moodleexport/ForumExport.php index 36cda8a9a8b..5044d3ea9d3 100644 --- a/main/inc/lib/moodleexport/ForumExport.php +++ b/main/inc/lib/moodleexport/ForumExport.php @@ -45,7 +45,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void */ public function getData(int $forumId, int $sectionId): ?array { - $forum = $this->course->resources['forum'][$forumId]->obj; + $forum = $this->course->resources[RESOURCE_FORUM][$forumId]->obj; $adminData = MoodleExport::getAdminUserData(); $adminId = $adminData['id']; @@ -78,14 +78,17 @@ public function getData(int $forumId, int $sectionId): ?array } } - $fileIds = []; + $name = $forum->forum_title ?? ''; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_FORUM, $forumId, $name); + } return [ 'id' => $forumId, 'moduleid' => $forumId, 'modulename' => 'forum', 'contextid' => $this->course->info['real_id'], - 'name' => $forum->forum_title, + 'name' => $name, 'description' => $forum->forum_comment, 'timecreated' => time(), 'timemodified' => time(), @@ -94,7 +97,7 @@ public function getData(int $forumId, int $sectionId): ?array 'userid' => $adminId, 'threads' => $threads, 'users' => [$adminId], - 'files' => $fileIds, + 'files' => [], ]; } diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index e943b3ed832..9c557fcc853 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -407,15 +407,27 @@ private function createMoodleBackupXml(string $destinationDir, int $version): vo } /** - * Get all sections from the course. + * Get all sections from the course ordered by LP display_order. */ private function getSections(): array { $sectionExport = new SectionExport($this->course); $sections = []; - foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { - if ($learnpath->lp_type == '1') { + // Safety: if there is no learnpath resource, return only the general section + $learnpaths = $this->course->resources[RESOURCE_LEARNPATH] ?? []; + + // Sort LPs by display_order to respect the order defined in c_lp + usort($learnpaths, static function ($a, $b): int { + $aOrder = (int) ($a->display_order ?? 0); + $bOrder = (int) ($b->display_order ?? 0); + + return $aOrder <=> $bOrder; + }); + + foreach ($learnpaths as $learnpath) { + // We only export "real" LPs (type 1) + if ((int) $learnpath->lp_type === 1) { $sections[] = $sectionExport->getSectionData($learnpath); } } @@ -437,116 +449,325 @@ private function getSections(): array /** * Get all activities from the course. + * Activities are ordered by learnpath display_order when available. */ private function getActivities(): array { - $activities = []; + $activities = []; $glossaryAdded = false; - $documentsFolder = [ - 'id' => 0, + // ----------------------------------------------------------------- + // 1) Build LP index: titles + display_order per section/type/resource + // ----------------------------------------------------------------- + $lpIndex = []; + if (!empty($this->course->resources[RESOURCE_LEARNPATH])) { + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { + $sid = (int) $lp->source_id; + + foreach ($lp->items ?? [] as $it) { + $type = $it['item_type'] ?? ''; + if ($type === 'student_publication') { + $type = 'assign'; + } elseif ($type === 'link') { + $type = 'url'; + } elseif ($type === 'survey') { + $type = 'feedback'; + } elseif ($type === 'document') { + $type = 'document'; + } + + $rid = $it['path'] ?? ''; + $title = $it['title'] ?? ''; + $order = isset($it['display_order']) ? (int) $it['display_order'] : 0; + + $entry = [ + 'title' => $title, + 'order' => $order, + ]; + + if (ctype_digit((string) $rid)) { + $rid = (int) $rid; + + // If the same resource appears multiple times, keep the lowest order. + if (!isset($lpIndex[$sid][$type]['id'][$rid])) { + $lpIndex[$sid][$type]['id'][$rid] = $entry; + } else { + $existingOrder = (int) ($lpIndex[$sid][$type]['id'][$rid]['order'] ?? 0); + if ($order > 0 && ($existingOrder === 0 || $order < $existingOrder)) { + $lpIndex[$sid][$type]['id'][$rid]['order'] = $order; + } + // Keep the first title to avoid random renames. + } + } else { + $rid = (string) $rid; + + if (!isset($lpIndex[$sid][$type]['path'][$rid])) { + $lpIndex[$sid][$type]['path'][$rid] = $entry; + } else { + $existingOrder = (int) ($lpIndex[$sid][$type]['path'][$rid]['order'] ?? 0); + if ($order > 0 && ($existingOrder === 0 || $order < $existingOrder)) { + $lpIndex[$sid][$type]['path'][$rid]['order'] = $order; + } + } + } + } + } + } + + // Helper: get title from LP index. + $titleFromLp = function (int $sectionId, string $moduleName, int $resourceId, string $fallback) use ($lpIndex) { + $type = in_array($moduleName, ['page', 'resource'], true) ? 'document' : $moduleName; + + $entry = $lpIndex[$sectionId][$type]['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['title'])) { + return $entry['title']; + } + + if ($type === 'assign') { + $entry = $lpIndex[$sectionId]['student_publication']['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['title'])) { + return $entry['title']; + } + } + + if ($type === 'document') { + $doc = \DocumentManager::get_document_data_by_id($resourceId, $this->course->code); + if (!empty($doc['path'])) { + $p = (string) $doc['path']; + foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { + $entry = $lpIndex[$sectionId]['document']['path'][$cand] ?? null; + if (is_array($entry) && !empty($entry['title'])) { + return $entry['title']; + } + } + } + } + + return $fallback; + }; + + // Helper: get display_order from LP index. + $orderFromLp = function (int $sectionId, string $moduleName, int $resourceId) use ($lpIndex): int { + $type = in_array($moduleName, ['page', 'resource'], true) ? 'document' : $moduleName; + + $entry = $lpIndex[$sectionId][$type]['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['order'])) { + return (int) $entry['order']; + } + + if ($type === 'assign') { + $entry = $lpIndex[$sectionId]['student_publication']['id'][$resourceId] ?? null; + if (is_array($entry) && !empty($entry['order'])) { + return (int) $entry['order']; + } + } + + if ($type === 'document') { + $doc = \DocumentManager::get_document_data_by_id($resourceId, $this->course->code); + if (!empty($doc['path'])) { + $p = (string) $doc['path']; + foreach ([$p, 'document/'.$p, '/'.$p] as $cand) { + $entry = $lpIndex[$sectionId]['document']['path'][$cand] ?? null; + if (is_array($entry) && !empty($entry['order'])) { + return (int) $entry['order']; + } + } + } + } + + return 0; + }; + + // ----------------------------------------------------------------- + // 2) "Documents" folder pseudo-activity (section 0) + // ----------------------------------------------------------------- + $activities[] = [ + 'id' => ActivityExport::DOCS_MODULE_ID, 'sectionid' => 0, - 'modulename' => 'folder', - 'moduleid' => 0, - 'title' => 'Documents', + 'modulename'=> 'folder', + 'moduleid' => ActivityExport::DOCS_MODULE_ID, + 'title' => 'Documents', + 'order' => 0, ]; - $activities[] = $documentsFolder; + + // ----------------------------------------------------------------- + // 3) Loop over all course resources (original logic) + // ----------------------------------------------------------------- $htmlPageIds = []; foreach ($this->course->resources as $resourceType => $resources) { foreach ($resources as $resource) { $exportClass = null; - $moduleName = ''; - $title = ''; - $id = 0; + $moduleName = ''; + $title = ''; + $id = 0; + $sectionId = 0; - // Handle quizzes + // QUIZ if ($resourceType === RESOURCE_QUIZ && $resource->obj->iid > 0) { $exportClass = QuizExport::class; - $moduleName = 'quiz'; - $id = $resource->obj->iid; - $title = $resource->obj->title; + $moduleName = 'quiz'; + $id = (int) $resource->obj->iid; + $title = $resource->obj->title ?? ''; + $sectionId = (new QuizExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle links - if ($resourceType === RESOURCE_LINK && $resource->source_id > 0) { + // URL + elseif ($resourceType === RESOURCE_LINK && $resource->source_id > 0) { $exportClass = UrlExport::class; - $moduleName = 'url'; - $id = $resource->source_id; - $title = $resource->title; + $moduleName = 'url'; + $id = (int) $resource->source_id; + $title = $resource->title ?? ''; + $sectionId = (new UrlExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle glossaries + // GLOSSARY (only one) elseif ($resourceType === RESOURCE_GLOSSARY && $resource->glossary_id > 0 && !$glossaryAdded) { - $exportClass = GlossaryExport::class; - $moduleName = 'glossary'; - $id = 1; - $title = get_lang('Glossary'); + $exportClass = GlossaryExport::class; + $moduleName = 'glossary'; + $id = 1; + $title = get_lang('Glossary'); + $sectionId = 0; $glossaryAdded = true; } - // Handle forums + // FORUM elseif ($resourceType === RESOURCE_FORUM && $resource->source_id > 0) { $exportClass = ForumExport::class; - $moduleName = 'forum'; - $id = $resource->obj->iid; - $title = $resource->obj->forum_title; + $moduleName = 'forum'; + $id = (int) $resource->obj->iid; + $title = $resource->obj->forum_title ?? ''; + $sectionId = (new ForumExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle documents (HTML pages) + // DOCUMENTS elseif ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { - $document = \DocumentManager::get_document_data_by_id($resource->source_id, $this->course->code); - if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION) && substr_count($resource->path, '/') === 1) { + $document = \DocumentManager::get_document_data_by_id( + $resource->source_id, + $this->course->code + ); + $ext = strtolower(pathinfo($document['path'] ?? '', PATHINFO_EXTENSION)); + + // HTML → Moodle "page" + if ($ext === 'html' || $ext === 'htm') { $exportClass = PageExport::class; - $moduleName = 'page'; - $id = $resource->source_id; - $title = $document['title']; + $moduleName = 'page'; + $id = (int) $resource->source_id; + $title = $document['title'] ?? ''; + $sectionId = (new PageExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); $htmlPageIds[] = $id; } - if ('file' === $resource->file_type && !in_array($resource->source_id, $htmlPageIds)) { + + // Other files → Moodle "resource" (but not if already exported as page) + if ($resource->file_type === 'file' + && !in_array($resource->source_id, $htmlPageIds, true) + ) { $resourceExport = new ResourceExport($this->course); - if ($resourceExport->getSectionIdForActivity($resource->source_id, $resourceType) > 0) { - $isRoot = substr_count($resource->path, '/') === 1; - if ($isRoot) { - $exportClass = ResourceExport::class; - $moduleName = 'resource'; - $id = $resource->source_id; - $title = $resource->title; - } + $sectionTmp = $resourceExport + ->getSectionIdForActivity((int) $resource->source_id, $resourceType); + + if ($sectionTmp > 0) { + $exportClass = ResourceExport::class; + $moduleName = 'resource'; + $id = (int) $resource->source_id; + $title = $resource->title ?? ''; + $sectionId = $sectionTmp; + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } } } - // Handle course introduction (page) - elseif ($resourceType === RESOURCE_TOOL_INTRO && $resource->source_id == 'course_homepage') { + // INTRODUCTION → Moodle "page" + elseif ($resourceType === RESOURCE_TOOL_INTRO + && $resource->source_id === 'course_homepage' + ) { $exportClass = PageExport::class; - $moduleName = 'page'; - $id = 0; - $title = get_lang('Introduction'); + $moduleName = 'page'; + $id = 0; + $title = get_lang('Introduction'); + $sectionId = 0; } - // Handle assignments (work) + // ASSIGN elseif ($resourceType === RESOURCE_WORK && $resource->source_id > 0) { $exportClass = AssignExport::class; - $moduleName = 'assign'; - $id = $resource->source_id; - $title = $resource->params['title'] ?? ''; + $moduleName = 'assign'; + $id = (int) $resource->source_id; + $title = $resource->params['title'] ?? ''; + $sectionId = (new AssignExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Handle feedback (survey) + // FEEDBACK / SURVEY elseif ($resourceType === RESOURCE_SURVEY && $resource->source_id > 0) { $exportClass = FeedbackExport::class; - $moduleName = 'feedback'; - $id = $resource->source_id; - $title = $resource->params['title'] ?? ''; + $moduleName = 'feedback'; + $id = (int) $resource->source_id; + $title = $resource->params['title'] ?? ''; + $sectionId = (new FeedbackExport($this->course)) + ->getSectionIdForActivity($id, $resourceType); + $title = $titleFromLp($sectionId, $moduleName, $id, $title); } - // Add the activity if the class and module name are set if ($exportClass && $moduleName) { - $exportInstance = new $exportClass($this->course); + $order = $orderFromLp($sectionId, $moduleName, $id); + $activities[] = [ - 'id' => $id, - 'sectionid' => $exportInstance->getSectionIdForActivity($id, $resourceType), - 'modulename' => $moduleName, - 'moduleid' => $id, - 'title' => $title, + 'id' => $id, + 'sectionid' => $sectionId, + 'modulename'=> $moduleName, + 'moduleid' => $id, + 'title' => $title, + 'order' => $order, ]; } } } + // ----------------------------------------------------------------- + // 4) Sort activities per section by LP order (display_order) + // ----------------------------------------------------------------- + if (!empty($activities)) { + $grouped = []; + $seqBySec = []; + + foreach ($activities as $activity) { + $sid = (int) ($activity['sectionid'] ?? 0); + + if (!isset($grouped[$sid])) { + $grouped[$sid] = []; + $seqBySec[$sid] = 0; + } + + $order = (int) ($activity['order'] ?? 0); + if ($order <= 0) { + // Keep relative insertion order for items without LP order, + // but make sure they come *after* the LP-ordered items. + $seqBySec[$sid]++; + $order = 1000 + $seqBySec[$sid]; + } + + $activity['_sort'] = $order; + $grouped[$sid][] = $activity; + } + + $activities = []; + foreach ($grouped as $sid => $list) { + usort( + $list, + static function (array $a, array $b): int { + return $a['_sort'] <=> $b['_sort']; + } + ); + + foreach ($list as $item) { + unset($item['order'], $item['_sort']); + $activities[] = $item; + } + } + } + return $activities; } @@ -854,4 +1075,57 @@ private function exportBackupSettings(array $sections, array $activities): array return $settings; } + + /** + * Maps module name to item_type of c_lp_item. + * (c_lp_item.item_type: document, quiz, link, forum, student_publication, survey) + */ + private function mapToLpItemType(string $moduleOrItemType): string + { + switch ($moduleOrItemType) { + case 'page': + case 'resource': + return 'document'; + case 'assign': + return 'student_publication'; + case 'url': + return 'link'; + case 'feedback': + return 'survey'; + default: + return $moduleOrItemType; // quiz, forum... + } + } + + /** Index titles by section + type + id from the LP items (c_lp_item.title). */ + private function buildLpTitleIndex(): array + { + $idx = []; + if (empty($this->course->resources[RESOURCE_LEARNPATH])) { + return $idx; + } + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) { + $sid = (int) $lp->source_id; + if (empty($lp->items)) { + continue; + } + foreach ($lp->items as $it) { + $type = $this->mapToLpItemType($it['item_type']); + $rid = (string) $it['path']; + $idx[$sid][$type][$rid] = $it['title'] ?? ''; + } + } + return $idx; + } + + /** Returns the LP title if it exists; otherwise, use the fallback. */ + private function titleFromLp(array $idx, int $sectionId, string $moduleName, int $resourceId, string $fallback): string + { + if ($sectionId <= 0) { + return $fallback; + } + $type = $this->mapToLpItemType($moduleName); + $rid = (string) $resourceId; + return $idx[$sectionId][$type][$rid] ?? $fallback; + } } diff --git a/main/inc/lib/moodleexport/PageExport.php b/main/inc/lib/moodleexport/PageExport.php index f9601b5e588..edd49a66b0e 100644 --- a/main/inc/lib/moodleexport/PageExport.php +++ b/main/inc/lib/moodleexport/PageExport.php @@ -111,12 +111,17 @@ public function getData(int $pageId, int $sectionId): ?array $pageResources = $this->course->resources[RESOURCE_DOCUMENT] ?? []; foreach ($pageResources as $page) { if ($page->source_id == $pageId) { + $name = $page->title ?? ''; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $page->source_id, $name); + } + return [ 'id' => $page->source_id, 'moduleid' => $page->source_id, 'modulename' => 'page', 'contextid' => $contextid, - 'name' => $page->title, + 'name' => $name, 'intro' => $page->comment ?? '', 'content' => $this->normalizeContent($this->getPageContent($page)), 'sectionid' => $sectionId, diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php index a33d8a9140b..9549e56364f 100644 --- a/main/inc/lib/moodleexport/QuizExport.php +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -49,19 +49,23 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void */ public function getData(int $quizId, int $sectionId): array { - $quizResources = $this->course->resources[RESOURCE_QUIZ]; + $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? []; foreach ($quizResources as $quiz) { if ($quiz->obj->iid == -1) { continue; } - - if ($quiz->obj->iid == $quizId) { + if ((int) $quiz->obj->iid === $quizId) { $contextid = $quiz->obj->c_id; + $name = $quiz->obj->title ?? ''; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_QUIZ, $quizId, $name); + } + return [ 'id' => $quiz->obj->iid, - 'name' => $quiz->obj->title, + 'name' => $name, 'intro' => $quiz->obj->description, 'timeopen' => $quiz->obj->start_time ?? 0, 'timeclose' => $quiz->obj->end_time ?? 0, @@ -105,6 +109,7 @@ public function getData(int $quizId, int $sectionId): array 'completionpass' => $quiz->obj->completionpass ?? 0, 'completionminattempts' => $quiz->obj->completionminattempts ?? 0, 'allowofflineattempts' => $quiz->obj->allowofflineattempts ?? 0, + 'navmethod' => $quiz->obj->navmethod ?? 'free', 'users' => [], 'files' => [], ]; diff --git a/main/inc/lib/moodleexport/ResourceExport.php b/main/inc/lib/moodleexport/ResourceExport.php index 68fad4e41e4..1c6c97e1860 100644 --- a/main/inc/lib/moodleexport/ResourceExport.php +++ b/main/inc/lib/moodleexport/ResourceExport.php @@ -46,12 +46,17 @@ public function getData(int $resourceId, int $sectionId): array { $resource = $this->course->resources[RESOURCE_DOCUMENT][$resourceId]; + $name = $resource->title; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $resourceId, $name); + } + return [ 'id' => $resourceId, 'moduleid' => $resource->source_id, 'modulename' => 'resource', 'contextid' => $resource->source_id, - 'name' => $resource->title, + 'name' => $name, 'intro' => $resource->comment ?? '', 'sectionid' => $sectionId, 'sectionnumber' => 1, diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index 6cf7b2bfbc4..948b6c33f21 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -113,14 +113,23 @@ public function getActivitiesForGeneral(): array $activities = $this->getActivitiesForSection($generalLearnpath, true); - if (!in_array('folder', array_column($activities, 'modulename'))) { - $activities[] = [ - 'id' => 0, - 'moduleid' => 0, - 'modulename' => 'folder', - 'name' => 'Documents', - 'sectionid' => 0, - ]; + // Ensure we always have one "Documents" folder activity at the top of the general section. + $hasFolder = false; + foreach ($activities as $activity) { + if ($activity['modulename'] === 'folder') { + $hasFolder = true; + break; + } + } + + if (!$hasFolder) { + array_unshift($activities, [ + 'id' => ActivityExport::DOCS_MODULE_ID, + 'moduleid' => ActivityExport::DOCS_MODULE_ID, + 'type' => 'folder', + 'modulename'=> 'folder', + 'name' => 'Documents', + ]); } return $activities; @@ -158,14 +167,32 @@ public function getSectionData(object $learnpath): array } /** - * Get the activities for a specific section. + * Get the activities for a specific section (learnpath). + * + * Items are sorted using c_lp_item.display_order so that the order + * in Moodle matches the order defined in the Chamilo LP. */ public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array { $activities = []; - $sectionId = $isGeneral ? 0 : $learnpath->source_id; + $sectionId = $isGeneral ? 0 : (int) $learnpath->source_id; + + $items = $learnpath->items ?? []; - foreach ($learnpath->items as $item) { + // Ensure items follow the same order as in Chamilo (c_lp_item.display_order). + usort($items, static function (array $a, array $b): int { + $aOrder = $a['display_order'] ?? 0; + $bOrder = $b['display_order'] ?? 0; + + if ($aOrder === $bOrder) { + // Fallback to id to keep a deterministic order. + return (int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0); + } + + return $aOrder <=> $bOrder; + }); + + foreach ($items as $item) { $this->addActivityToList($item, $sectionId, $activities); } @@ -225,7 +252,7 @@ private function isItemInLearnpath(object $item, string $type): bool */ private function addActivityToList(array $item, int $sectionId, array &$activities): void { - static $documentsFolderAdded = false; + /*static $documentsFolderAdded = false; if (!$documentsFolderAdded && $sectionId === 0) { $activities[] = [ 'id' => 0, @@ -235,7 +262,7 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti 'name' => 'Documents', ]; $documentsFolderAdded = true; - } + }*/ $activityData = null; $activityClassMap = [ diff --git a/main/inc/lib/moodleexport/UrlExport.php b/main/inc/lib/moodleexport/UrlExport.php index c92b96a0fc5..3652437a93d 100644 --- a/main/inc/lib/moodleexport/UrlExport.php +++ b/main/inc/lib/moodleexport/UrlExport.php @@ -47,13 +47,18 @@ public function getData(int $activityId, int $sectionId): ?array // Extract the URL information from the course data $url = $this->course->resources['link'][$activityId]; + $name = $url->title; + if ($sectionId > 0) { + $name = $this->lpItemTitle($sectionId, RESOURCE_LINK, $activityId, $name); + } + // Return the URL data formatted for export return [ 'id' => $activityId, 'moduleid' => $activityId, 'modulename' => 'url', 'contextid' => $this->course->info['real_id'], - 'name' => $url->title, + 'name' => $name, 'description' => $url->description, 'externalurl' => $url->url, 'timecreated' => time(),