course = $course; $this->activitiesBySection = $activitiesBySection; } /** * Export a section and its activities to the specified directory. */ public function exportSection(int $sectionId, string $exportDir): void { $sectionDir = $exportDir."/sections/section_{$sectionId}"; if (!is_dir($sectionDir)) { mkdir($sectionDir, api_get_permissions_for_new_directories(), true); } if ($sectionId > 0) { $learnpath = $this->getLearnpathById($sectionId); if ($learnpath === null) { throw new Exception("Learnpath with ID $sectionId not found."); } $sectionData = $this->getSectionData($learnpath); } else { $sectionData = [ 'id' => 0, 'number' => 0, 'name' => $this->sanitizeText((string) get_lang('General')), 'summary' => (string) get_lang('GeneralResourcesCourse'), 'sequence' => 0, 'visible' => 1, 'timemodified' => time(), 'activities' => $this->getActivitiesForGeneral(), ]; } $this->createSectionXml($sectionData, $sectionDir); $this->createInforefXml($sectionData, $sectionDir); $this->exportActivities($sectionData['activities'], $exportDir, $sectionId); } /** * Get all general items not linked to any lesson (learnpath). */ public function getGeneralItems(): array { $generalItems = []; $resourceTypes = [ RESOURCE_DOCUMENT => 'source_id', RESOURCE_QUIZ => 'source_id', RESOURCE_GLOSSARY => 'glossary_id', RESOURCE_LINK => 'source_id', RESOURCE_WORK => 'source_id', RESOURCE_FORUM => 'source_id', RESOURCE_SURVEY => 'source_id', RESOURCE_TOOL_INTRO => 'source_id', ]; foreach ($resourceTypes as $resourceType => $idKey) { if (!empty($this->course->resources[$resourceType])) { foreach ($this->course->resources[$resourceType] as $id => $resource) { if (!$this->isItemInLearnpath($resource, $resourceType)) { $title = $resourceType === RESOURCE_WORK ? (string) ($resource->params['title'] ?? '') : (string) ($resource->title ?? $resource->name ?? ''); $generalItems[] = [ 'id' => $resource->$idKey, 'item_type' => $resourceType, 'path' => $id, 'title' => $title, ]; } } } } return $generalItems; } /** * Get the activities for the general section. */ public function getActivitiesForGeneral(): array { if (isset($this->activitiesBySection[0]) && is_array($this->activitiesBySection[0])) { return $this->activitiesBySection[0]; } $generalLearnpath = (object) [ 'items' => $this->getGeneralItems(), 'source_id' => 0, ]; $activities = $this->getActivitiesForSection($generalLearnpath, true); if (!in_array('folder', array_column($activities, 'modulename'), true)) { $activities[] = [ 'id' => 0, 'moduleid' => 0, 'modulename' => 'folder', 'name' => 'Documents', 'sectionid' => 0, ]; } return $activities; } /** * Get the learnpath object by its ID. */ public function getLearnpathById(int $sectionId): ?object { foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { if ((int) $learnpath->source_id === $sectionId) { return $learnpath; } } return null; } /** * Get section data for a learnpath. */ public function getSectionData(object $learnpath): array { $sectionId = (int) $learnpath->source_id; return [ 'id' => $sectionId, 'number' => (int) ($learnpath->display_order ?? 0), 'name' => $this->sanitizeText((string) ($learnpath->name ?? '')), 'summary' => (string) ($learnpath->description ?? ''), 'sequence' => $sectionId, 'visible' => 1, 'timemodified' => !empty($learnpath->modified_on) ? (int) strtotime((string) $learnpath->modified_on) : time(), 'activities' => $this->getActivitiesForSection($learnpath), ]; } /** * Get the activities for a specific section. */ public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array { $sectionId = $isGeneral ? 0 : (int) $learnpath->source_id; if (isset($this->activitiesBySection[$sectionId]) && is_array($this->activitiesBySection[$sectionId])) { return $this->activitiesBySection[$sectionId]; } $activities = []; foreach ($learnpath->items as $item) { $this->addActivityToList($item, $sectionId, $activities); } return $activities; } /** * Export the activities of a section. */ private function exportActivities(array $activities, string $exportDir, int $sectionId): void { $exportClasses = [ 'quiz' => QuizExport::class, 'glossary' => GlossaryExport::class, 'url' => UrlExport::class, 'assign' => AssignExport::class, 'forum' => ForumExport::class, 'page' => PageExport::class, 'resource' => ResourceExport::class, 'folder' => FolderExport::class, 'feedback' => FeedbackExport::class, ]; foreach ($activities as $activity) { $moduleName = $activity['modulename']; if (isset($exportClasses[$moduleName])) { $exportClass = new $exportClasses[$moduleName]($this->course); $exportClass->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); } else { throw new Exception("Export for module '$moduleName' is not supported."); } } } /** * Normalize resource / LP item types for reliable comparison. */ private function normalizeItemTypeForLpComparison(string $type): string { switch ($type) { case 'student_publication': case 'work': return 'work'; case 'link': case 'url': return 'link'; case 'survey': case 'feedback': return 'survey'; default: return $type; } } /** * Check if an item is associated with any learnpath. */ private function isItemInLearnpath(object $item, string $type): bool { if (empty($this->course->resources[RESOURCE_LEARNPATH])) { return false; } $normalizedType = $this->normalizeItemTypeForLpComparison($type); $itemSourceId = isset($item->source_id) ? (string) $item->source_id : ''; foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { if (empty($learnpath->items)) { continue; } foreach ($learnpath->items as $learnpathItem) { $lpType = isset($learnpathItem['item_type']) ? $this->normalizeItemTypeForLpComparison((string) $learnpathItem['item_type']) : ''; if ($lpType !== $normalizedType) { continue; } $lpPath = isset($learnpathItem['path']) ? (string) $learnpathItem['path'] : ''; if ($itemSourceId !== '' && $lpPath === $itemSourceId) { return true; } if ($normalizedType === 'document' && $itemSourceId !== '' && ctype_digit($itemSourceId)) { $doc = \DocumentManager::get_document_data_by_id((int) $itemSourceId, $this->course->code); if (!empty($doc['path'])) { $docPath = (string) $doc['path']; $candidates = [$docPath, 'document/'.$docPath, '/'.$docPath]; foreach ($candidates as $candidate) { if ($lpPath === $candidate) { return true; } } } } } } return false; } /** * Add an activity to the activities list. */ private function addActivityToList(array $item, int $sectionId, array &$activities): void { static $documentsFolderAdded = false; if (!$documentsFolderAdded && $sectionId === 0) { $activities[] = [ 'id' => 0, 'moduleid' => 0, 'type' => 'folder', 'modulename' => 'folder', 'name' => 'Documents', ]; $documentsFolderAdded = true; } $activityData = null; $activityClassMap = [ 'quiz' => QuizExport::class, 'glossary' => GlossaryExport::class, 'url' => UrlExport::class, 'assign' => AssignExport::class, 'forum' => ForumExport::class, 'page' => PageExport::class, 'resource' => ResourceExport::class, 'feedback' => FeedbackExport::class, ]; if (($item['id'] ?? null) === 'course_homepage') { $item['item_type'] = 'page'; $item['path'] = 0; } $itemType = ($item['item_type'] ?? '') === 'link' ? 'url' : ((($item['item_type'] ?? '') === 'work' || ($item['item_type'] ?? '') === 'student_publication') ? 'assign' : ((($item['item_type'] ?? '') === 'survey') ? 'feedback' : ($item['item_type'] ?? ''))); switch ($itemType) { case 'quiz': case 'glossary': case 'assign': case 'url': case 'forum': case 'feedback': case 'page': $activityId = $itemType === 'glossary' ? 1 : (int) ($item['path'] ?? 0); $exportClass = $activityClassMap[$itemType]; $exportInstance = new $exportClass($this->course); $activityData = $exportInstance->getData($activityId, $sectionId); break; case 'document': $documentId = (int) ($item['path'] ?? 0); $document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code); if ($document) { $documentType = $this->getDocumentType((string) $document['filetype'], (string) $document['path']); if ($sectionId > 0 && $documentType && isset($activityClassMap[$documentType])) { $activityClass = $activityClassMap[$documentType]; $exportInstance = new $activityClass($this->course); $activityData = $exportInstance->getData($documentId, $sectionId); } elseif ($sectionId === 0 && $documentType === 'page') { $activityClass = $activityClassMap['page']; $exportInstance = new $activityClass($this->course); $activityData = $exportInstance->getData($documentId, $sectionId); } } break; } if (!empty($activityData) && $sectionId > 0) { $lpItemId = isset($item['id']) ? (int) $item['id'] : 0; $modName = (string) ($activityData['modulename'] ?? ''); if ($lpItemId > 0 && !in_array($modName, ['folder', 'glossary'], true)) { $activityData['moduleid'] = 900000000 + $lpItemId; } } if ($activityData) { $activities[] = [ 'id' => $activityData['id'], 'moduleid' => $activityData['moduleid'], 'type' => $item['item_type'], 'modulename' => $activityData['modulename'], 'name' => $this->sanitizeText((string) ($activityData['name'] ?? '')), ]; } } /** * Determine the document type based on filetype and path. */ private function getDocumentType(string $filetype, string $path): ?string { $ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION)); if ($ext === 'html' || $ext === 'htm') { return 'page'; } if ($filetype === 'file') { return 'resource'; } return null; } /** * Create the section.xml file. */ private function createSectionXml(array $sectionData, string $destinationDir): void { $sequence = implode(',', array_column($sectionData['activities'], 'moduleid')); $xmlContent = ''.PHP_EOL; $xmlContent .= '
'.PHP_EOL; $xmlContent .= ' '.$sectionData['number'].''.PHP_EOL; $xmlContent .= ' '.htmlspecialchars((string) $sectionData['name']).''.PHP_EOL; $xmlContent .= ' '.htmlspecialchars((string) $sectionData['summary']).''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.$sequence.''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.$sectionData['timemodified'].''.PHP_EOL; $xmlContent .= '
'.PHP_EOL; file_put_contents($destinationDir.'/section.xml', $xmlContent); } /** * Create the inforef.xml file for the section. */ private function createInforefXml(array $sectionData, string $destinationDir): void { $xmlContent = ''.PHP_EOL; $xmlContent .= ''.PHP_EOL; foreach ($sectionData['activities'] as $activity) { $refId = $activity['moduleid'] ?? $activity['id']; $xmlContent .= ' '.htmlspecialchars((string) ($activity['name'] ?? '')).''.PHP_EOL; } $xmlContent .= ''.PHP_EOL; file_put_contents($destinationDir.'/inforef.xml', $xmlContent); } /** * Sanitize section or activity titles for Moodle. */ private function sanitizeText(string $raw, int $maxLen = 255): string { $text = trim($raw); if ($text === '') { return ''; } $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = strip_tags($text); $text = preg_replace('/\s+/u', ' ', $text); $text = trim($text); if ($text === '') { return ''; } if (function_exists('mb_strlen') && function_exists('mb_substr')) { if (mb_strlen($text, 'UTF-8') > $maxLen) { $text = mb_substr($text, 0, $maxLen, 'UTF-8'); } } elseif (strlen($text) > $maxLen) { $text = substr($text, 0, $maxLen); } return $text; } }