468 lines
16 KiB
PHP
468 lines
16 KiB
PHP
<?php
|
|
|
|
/* For licensing terms, see /license.txt */
|
|
|
|
namespace moodleexport;
|
|
|
|
use Exception;
|
|
|
|
/**
|
|
* Class SectionExport.
|
|
* Handles the export of course sections and their activities.
|
|
*
|
|
* @package moodleexport
|
|
*/
|
|
class SectionExport
|
|
{
|
|
private $course;
|
|
private $activitiesBySection = [];
|
|
|
|
/**
|
|
* Constructor to initialize the course object.
|
|
*
|
|
* @param object $course The course object to be exported.
|
|
*/
|
|
public function __construct($course, array $activitiesBySection = [])
|
|
{
|
|
$this->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 = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
|
|
$xmlContent .= '<section id="'.$sectionData['id'].'">'.PHP_EOL;
|
|
$xmlContent .= ' <number>'.$sectionData['number'].'</number>'.PHP_EOL;
|
|
$xmlContent .= ' <name>'.htmlspecialchars((string) $sectionData['name']).'</name>'.PHP_EOL;
|
|
$xmlContent .= ' <summary>'.htmlspecialchars((string) $sectionData['summary']).'</summary>'.PHP_EOL;
|
|
$xmlContent .= ' <summaryformat>1</summaryformat>'.PHP_EOL;
|
|
$xmlContent .= ' <sequence>'.$sequence.'</sequence>'.PHP_EOL;
|
|
$xmlContent .= ' <visible>1</visible>'.PHP_EOL;
|
|
$xmlContent .= ' <timemodified>'.$sectionData['timemodified'].'</timemodified>'.PHP_EOL;
|
|
$xmlContent .= '</section>'.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 = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
|
|
$xmlContent .= '<inforef>'.PHP_EOL;
|
|
|
|
foreach ($sectionData['activities'] as $activity) {
|
|
$refId = $activity['moduleid'] ?? $activity['id'];
|
|
$xmlContent .= ' <activity id="'.$refId.'">'.htmlspecialchars((string) ($activity['name'] ?? '')).'</activity>'.PHP_EOL;
|
|
}
|
|
|
|
$xmlContent .= '</inforef>'.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;
|
|
}
|
|
}
|