course = $course;
}
/**
* Abstract method for exporting the activity.
* Must be implemented by child classes.
*/
abstract public function export($activityId, $exportDir, $moduleId, $sectionId);
/**
* 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) {
$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;
}
}
}
}
}
}
return 0;
}
/**
* Prepares the directory for the activity.
*/
protected function prepareActivityDirectory(string $exportDir, string $activityType, int $moduleId): string
{
$activityDir = "{$exportDir}/activities/{$activityType}_{$moduleId}";
if (!is_dir($activityDir)) {
mkdir($activityDir, 0777, true);
}
return $activityDir;
}
/**
* Creates a generic XML file.
*/
protected function createXmlFile(string $fileName, string $xmlContent, string $directory): void
{
$filePath = $directory.'/'.$fileName.'.xml';
if (file_put_contents($filePath, $xmlContent) === false) {
throw new Exception("Error creating {$fileName}.xml");
}
}
/**
* Creates the module.xml file.
*/
protected function createModuleXml(array $data, string $directory): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.$data['modulename'].''.PHP_EOL;
$xmlContent .= ' '.$data['sectionid'].''.PHP_EOL;
$xmlContent .= ' '.$data['sectionnumber'].''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.time().''.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' 1'.PHP_EOL;
$xmlContent .= ' 1'.PHP_EOL;
$xmlContent .= ' 1'.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' 1'.PHP_EOL;
$xmlContent .= ' $@NULL@$'.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' $@NULL@$'.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('module', $xmlContent, $directory);
}
/**
* Creates the grades.xml file.
*/
protected function createGradesXml(array $data, string $directory): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('grades', $xmlContent, $directory);
}
/**
* Creates the inforef.xml file, referencing users and files associated with the activity.
*/
protected function createInforefXml(array $references, string $directory): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
if (isset($references['users']) && is_array($references['users'])) {
$xmlContent .= ' '.PHP_EOL;
foreach ($references['users'] as $userId) {
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.htmlspecialchars((string) $userId).''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
}
$xmlContent .= ' '.PHP_EOL;
}
if (isset($references['files']) && is_array($references['files'])) {
$xmlContent .= ' '.PHP_EOL;
foreach ($references['files'] as $file) {
$fileId = is_array($file) ? (int) ($file['id'] ?? 0) : (int) $file;
if ($fileId <= 0) {
continue;
}
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.$fileId.''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
}
$xmlContent .= ' '.PHP_EOL;
}
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('inforef', $xmlContent, $directory);
}
/**
* Creates the roles.xml file.
*/
protected function createRolesXml(array $activityData, string $directory): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('roles', $xmlContent, $directory);
}
/**
* Creates the filters.xml file for the activity.
*/
protected function createFiltersXml(array $activityData, string $destinationDir): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('filters', $xmlContent, $destinationDir);
}
/**
* Creates the grade_history.xml file for the activity.
*/
protected function createGradeHistoryXml(array $activityData, string $destinationDir): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('grade_history', $xmlContent, $destinationDir);
}
/**
* Creates the completion.xml file.
*/
protected function createCompletionXml(array $activityData, string $destinationDir): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' 0'.PHP_EOL;
$xmlContent .= ' 1'.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('completion', $xmlContent, $destinationDir);
}
/**
* Creates the comments.xml file.
*/
protected function createCommentsXml(array $activityData, string $destinationDir): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' This is a sample comment'.PHP_EOL;
$xmlContent .= ' Professor'.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('comments', $xmlContent, $destinationDir);
}
/**
* Creates the competencies.xml file.
*/
protected function createCompetenciesXml(array $activityData, string $destinationDir): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' Sample Competency'.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('competencies', $xmlContent, $destinationDir);
}
/**
* Creates the calendar.xml file.
*/
protected function createCalendarXml(array $activityData, string $destinationDir): void
{
$xmlContent = ''.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ' Due Date'.PHP_EOL;
$xmlContent .= ' '.time().''.PHP_EOL;
$xmlContent .= ' '.PHP_EOL;
$xmlContent .= ''.PHP_EOL;
$this->createXmlFile('calendar', $xmlContent, $destinationDir);
}
/**
* Creates a Moodle-safe activity name.
* - Strip HTML
* - Decode entities
* - Normalize whitespace
* - Truncate to a maximum length.
*/
protected function sanitizeMoodleActivityName(string $raw, int $maxLen = 255): string
{
$s = trim($raw);
if ($s === '') {
return '';
}
$s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$s = strip_tags($s);
$s = preg_replace('/\s+/u', ' ', $s);
$s = trim($s);
if ($s === '') {
return '';
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($s, 'UTF-8') > $maxLen) {
$s = mb_substr($s, 0, $maxLen, 'UTF-8');
}
} else {
if (strlen($s) > $maxLen) {
$s = substr($s, 0, $maxLen);
}
}
return $s;
}
/**
* Returns the title of the item in the LP if it exists, otherwise the 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 ?? '';
}
/**
* Extract embedded files from HTML content and normalize URLs to @@PLUGINFILE@@.
*
* Supported sources:
* - Course documents: /document/...
* - Platform assets: /main/default_course_document/... and /main/img/...
*
* @param callable $fileIdBuilder Receives the 1-based sequence and must return a unique file id.
*
* @return array{content:string, files:array>}
*/
protected function extractEmbeddedFilesAndNormalizeContent(
string $html,
int $contextId,
string $component,
string $fileArea,
int $itemId,
callable $fileIdBuilder
): array {
if ($html === '') {
return [
'content' => '',
'files' => [],
];
}
$courseInfo = api_get_course_info($this->course->code);
$adminId = (int) (MoodleExport::getAdminUserData()['id'] ?? 1);
$fileExport = new FileExport($this->course);
$files = [];
$seenSources = [];
$sequence = 0;
$normalizedHtml = preg_replace_callback(
'#
]+src=["\'](?[^"\']+)["\']#i',
function ($match) use (
$courseInfo,
$contextId,
$component,
$fileArea,
$itemId,
$fileIdBuilder,
$adminId,
$fileExport,
&$files,
&$seenSources,
&$sequence
) {
$src = (string) ($match['url'] ?? '');
$resolved = $this->resolveEmbeddedFileSource($src, $courseInfo);
if ($resolved === null) {
return $match[0];
}
$sourceKey = (string) $resolved['sourcekey'];
if (!isset($seenSources[$sourceKey])) {
$sequence++;
$fileId = (int) call_user_func($fileIdBuilder, $sequence);
$absolutePath = (string) $resolved['absolutepath'];
$filename = (string) $resolved['filename'];
$files[] = [
'id' => $fileId,
'contenthash' => is_file($absolutePath)
? sha1_file($absolutePath)
: hash('sha1', $filename),
'contextid' => $contextId,
'component' => $component,
'filearea' => $fileArea,
'itemid' => $itemId,
'filepath' => (string) $resolved['filepath'],
'documentpath' => (string) $resolved['documentpath'],
'absolutepath' => $absolutePath,
'filename' => $filename,
'userid' => $adminId,
'filesize' => is_file($absolutePath) ? (int) filesize($absolutePath) : 0,
'mimetype' => $fileExport->getMimeType($filename),
'status' => 0,
'timecreated' => time() - 3600,
'timemodified' => time(),
'source' => $filename,
'author' => 'Unknown',
'license' => 'allrightsreserved',
];
$seenSources[$sourceKey] = true;
}
return str_replace($src, '@@PLUGINFILE@@'.(string) $resolved['pluginfilepath'], $match[0]);
},
$html
);
return [
'content' => (string) $normalizedHtml,
'files' => $files,
];
}
/**
* Resolve an embeddable source URL into export metadata.
*
* @return array|null
*/
protected function resolveEmbeddedFileSource(string $src, array $courseInfo): ?array
{
$urlPath = $this->extractUrlPath($src);
if (preg_match('#/document(?P/[^"\']+)#', $urlPath, $m)) {
$documentRelativePath = (string) $m['path'];
$docId = \DocumentManager::get_document_id($courseInfo, $documentRelativePath);
if (empty($docId)) {
return null;
}
$document = \DocumentManager::get_document_data_by_id((int) $docId, $this->course->code);
if (empty($document)) {
return null;
}
$documentPath = (string) ($document['path'] ?? '');
if ($documentPath === '') {
return null;
}
return [
'sourcekey' => 'document:'.$docId,
'filepath' => $this->buildPluginFileDirectoryFromChamiloDocumentPath($documentPath),
'pluginfilepath' => $this->buildPluginFilePathFromChamiloDocumentPath($documentRelativePath),
'documentpath' => 'document'.$documentPath,
'absolutepath' => $this->buildChamiloDocumentAbsolutePath($documentPath),
'filename' => basename($documentPath),
];
}
if (preg_match('#(?P/main/(?:default_course_document|img)/[^"\']+)#', $urlPath, $m)) {
$assetPath = (string) $m['path'];
return [
'sourcekey' => 'static:'.$assetPath,
'filepath' => $this->buildPluginFileDirectoryFromStaticAssetPath($assetPath),
'pluginfilepath' => $this->buildPluginFilePathFromStaticAssetPath($assetPath),
'documentpath' => '',
'absolutepath' => $this->buildStaticAssetAbsolutePath($assetPath),
'filename' => basename($assetPath),
];
}
return null;
}
/**
* Extract the path part from a raw URL.
*/
protected function extractUrlPath(string $src): string
{
$decoded = html_entity_decode($src, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$path = (string) parse_url($decoded, PHP_URL_PATH);
return $path !== '' ? $path : $decoded;
}
/**
* Build the absolute path for one Chamilo course document.
*/
protected function buildChamiloDocumentAbsolutePath(string $documentPath): string
{
$normalizedPath = (string) preg_replace('#^/?document/#', '', str_replace('\\', '/', trim($documentPath)));
$normalizedPath = '/'.ltrim($normalizedPath, '/');
return rtrim((string) $this->course->path, '/').'/document'.$normalizedPath;
}
/**
* Build the absolute path for one static platform asset.
*/
protected function buildStaticAssetAbsolutePath(string $assetPath): string
{
return rtrim(api_get_path(SYS_PATH), '/').'/'.ltrim($assetPath, '/');
}
/**
* Build the pluginfile directory path from a Chamilo document path.
*/
protected function buildPluginFileDirectoryFromChamiloDocumentPath(string $documentPath): string
{
$relative = $this->stripChamiloDocumentPrefix($documentPath);
$relative = ltrim(str_replace('\\', '/', $relative), '/');
$dir = dirname($relative);
if ($dir === '.' || $dir === '/') {
return '/';
}
return '/'.trim($dir, '/').'/';
}
/**
* Build the pluginfile full path used in HTML content.
*/
protected function buildPluginFilePathFromChamiloDocumentPath(string $documentPath): string
{
$relative = $this->stripChamiloDocumentPrefix($documentPath);
$relative = ltrim(str_replace('\\', '/', $relative), '/');
return '/'.$relative;
}
/**
* Build the pluginfile directory path for one static platform asset.
*/
protected function buildPluginFileDirectoryFromStaticAssetPath(string $assetPath): string
{
$relative = ltrim(str_replace('\\', '/', trim($assetPath)), '/');
$dir = dirname($relative);
if ($dir === '.' || $dir === '/') {
return '/Static/';
}
return '/Static/'.trim($dir, '/').'/';
}
/**
* Build the pluginfile full path used in HTML content for one static platform asset.
*/
protected function buildPluginFilePathFromStaticAssetPath(string $assetPath): string
{
$relative = ltrim(str_replace('\\', '/', trim($assetPath)), '/');
return '/Static/'.$relative;
}
/**
* Remove the internal Chamilo "document/" prefix if present.
*/
protected function stripChamiloDocumentPrefix(string $path): string
{
$path = str_replace('\\', '/', trim($path));
$path = preg_replace('#^/?document/#', '', $path);
return (string) $path;
}
}