diff --git a/main/course_info/download.php b/main/course_info/download.php
index f393e3d28fc..6b863217778 100755
--- a/main/course_info/download.php
+++ b/main/course_info/download.php
@@ -27,8 +27,8 @@
$content_type = '';
$_cid = api_get_course_int_id();
-if (in_array($extension, ['xml', 'csv', 'imscc']) &&
- (api_is_platform_admin(true) || api_is_drh() || (CourseManager::is_course_teacher(api_get_user_id(), api_get_course_id())))
+if (in_array($extension, ['xml', 'csv', 'imscc', 'mbz']) &&
+ (api_is_platform_admin(true) || api_is_drh() || CourseManager::is_course_teacher(api_get_user_id(), api_get_course_id()))
) {
$content_type = 'application/force-download';
} elseif ('zip' === $extension && $_cid && (api_is_platform_admin(true) || api_is_course_admin())) {
diff --git a/main/course_info/maintenance.php b/main/course_info/maintenance.php
index 0575e081593..65b54db00b5 100755
--- a/main/course_info/maintenance.php
+++ b/main/course_info/maintenance.php
@@ -47,6 +47,12 @@
+
';
+ echo '
';
+ echo '
';
+ $form->display();
+ echo '
';
+ echo '
';
+ }
+}
+
+Display::display_footer();
diff --git a/main/inc/lib/document.lib.php b/main/inc/lib/document.lib.php
index 00208695960..ecd654c9c88 100644
--- a/main/inc/lib/document.lib.php
+++ b/main/inc/lib/document.lib.php
@@ -7533,6 +7533,66 @@ private static function getButtonDelete(
return $btn;
}
+ /**
+ * Retrieves all documents in a course by their parent folder ID.
+ *
+ * @param array $courseInfo Information about the course.
+ * @param int $parentId The ID of the parent folder.
+ * @param int $toGroupId (Optional) The ID of the group to filter by. Default is 0.
+ * @param int|null $toUserId (Optional) The ID of the user to filter by. Default is null.
+ * @param bool $canSeeInvisible (Optional) Whether to include invisible documents. Default is false.
+ * @param bool $search (Optional) Whether to perform a search or fetch all documents. Default is true.
+ * @param int $sessionId (Optional) The session ID to filter by. Default is 0.
+ *
+ * @return array List of documents that match the criteria.
+ */
+ public static function getAllDocumentsByParentId(
+ $courseInfo,
+ $parentId,
+ $toGroupId = 0,
+ $toUserId = null,
+ $canSeeInvisible = false,
+ $search = true,
+ $sessionId = 0
+ ) {
+ if (empty($courseInfo)) {
+ return [];
+ }
+
+ $tblDocument = Database::get_course_table(TABLE_DOCUMENT);
+
+ $parentId = (int) $parentId;
+
+ $sql = "SELECT path, filetype
+ FROM $tblDocument
+ WHERE id = $parentId
+ AND c_id = {$courseInfo['real_id']}";
+
+ $result = Database::query($sql);
+
+ if ($result === false || Database::num_rows($result) == 0) {
+ return [];
+ }
+
+ $parentRow = Database::fetch_array($result, 'ASSOC');
+ $parentPath = $parentRow['path'];
+ $filetype = $parentRow['filetype'];
+
+ if ($filetype !== 'folder') {
+ return [];
+ }
+
+ return self::getAllDocumentData(
+ $courseInfo,
+ $parentPath,
+ $toGroupId,
+ $toUserId,
+ $canSeeInvisible,
+ $search,
+ $sessionId
+ );
+ }
+
/**
* Include MathJax script in document.
*
diff --git a/main/inc/lib/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php
new file mode 100644
index 00000000000..11334b74d9b
--- /dev/null
+++ b/main/inc/lib/moodleexport/ActivityExport.php
@@ -0,0 +1,253 @@
+course = $course;
+ }
+
+ /**
+ * Abstract method for exporting the activity.
+ * Must be implemented by child classes.
+ */
+ abstract public function export($activityId, $exportDir, $moduleId, $sectionId);
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Get the section ID for a given activity ID.
+ */
+ public function getSectionIdForActivity(int $activityId, string $itemType): int
+ {
+ foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
+ foreach ($learnpath->items as $item) {
+ if ($item['item_type'] == $itemType && $item['path'] == $activityId) {
+ return $learnpath->source_id;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * 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
+ {
+ // Start the XML content
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+
+ // Add user references if provided
+ if (isset($references['users']) && is_array($references['users'])) {
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($references['users'] as $userId) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($userId) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+
+ // Add file references if provided
+ if (isset($references['files']) && is_array($references['files'])) {
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($references['files'] as $file) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($file['id']) . '' . 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);
+ }
+}
diff --git a/main/inc/lib/moodleexport/AssignExport.php b/main/inc/lib/moodleexport/AssignExport.php
new file mode 100644
index 00000000000..57e7b1ac925
--- /dev/null
+++ b/main/inc/lib/moodleexport/AssignExport.php
@@ -0,0 +1,196 @@
+prepareActivityDirectory($exportDir, 'assign', $moduleId);
+
+ // Retrieve assign data
+ $assignData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files for the assign
+ $this->createAssignXml($assignData, $assignDir);
+ $this->createModuleXml($assignData, $assignDir);
+ $this->createGradesXml($assignData, $assignDir);
+ $this->createGradingXml($assignData, $assignDir);
+ $this->createInforefXml($assignData, $assignDir);
+ $this->createGradeHistoryXml($assignData, $assignDir);
+ $this->createRolesXml($assignData, $assignDir);
+ $this->createCommentsXml($assignData, $assignDir);
+ $this->createCalendarXml($assignData, $assignDir);
+ $this->createFiltersXml($assignData, $assignDir);
+ }
+
+ /**
+ * Create the XML file for the assign activity.
+ */
+ private function createAssignXml(array $assignData, string $assignDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($assignData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . $assignData['duedate'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $assignData['gradingduedate'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $assignData['allowsubmissionsfromdate'] . '' . PHP_EOL;
+ $xmlContent .= ' 100' . PHP_EOL;
+ $xmlContent .= ' ' . $assignData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' none' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('assign', $xmlContent, $assignDir);
+ }
+
+ /**
+ * Create the grades.xml file for the assign activity.
+ */
+ protected function createGradesXml(array $data, string $directory): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($data['name']) . '' . PHP_EOL;
+ $xmlContent .= ' mod' . PHP_EOL;
+ $xmlContent .= ' '.$data['modulename'].'' . PHP_EOL;
+ $xmlContent .= ' ' . $data['id'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 100.00000' . PHP_EOL;
+ $xmlContent .= ' 0.00000' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' 0.00000' . PHP_EOL;
+ $xmlContent .= ' 1.00000' . PHP_EOL;
+ $xmlContent .= ' 0.00000' . PHP_EOL;
+ $xmlContent .= ' 0.00000' . PHP_EOL;
+ $xmlContent .= ' 0.23810' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 5' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $data['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $data['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('grades', $xmlContent, $directory);
+ }
+
+ /**
+ * Create the grading.xml file for the assign activity.
+ */
+ private function createGradingXml(array $data, string $assignDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' submissions' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('grading', $xmlContent, $assignDir);
+ }
+
+ /**
+ * Get all the data related to the assign activity.
+ */
+ public function getData(int $assignId, int $sectionId): ?array
+ {
+ $work = $this->course->resources[RESOURCE_WORK][$assignId];
+
+ $workFiles = getAllDocumentToWork($assignId, $this->course->info['real_id']);
+ $files = [];
+ if (!empty($workFiles)) {
+ foreach ($workFiles as $file) {
+ $docData = DocumentManager::get_document_data_by_id($file['document_id'], $this->course->info['code']);
+ if (!empty($docData)) {
+ $files[] = [
+ 'id' => $file['document_id'],
+ 'contenthash' => hash('sha1', basename($docData['path'])),
+ ];
+ }
+ }
+ }
+
+ $adminData = MoodleExport::getAdminUserData();
+ $adminId = $adminData['id'];
+
+ return [
+ 'id' => (int) $work->params['id'],
+ 'moduleid' => (int) $work->params['id'],
+ 'modulename' => 'assign',
+ 'contextid' => $this->course->info['real_id'],
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 0,
+ 'name' => htmlspecialchars($work->params['title']),
+ 'intro' => $work->params['description'],
+ 'duedate' => strtotime($work->params['sent_date']),
+ 'gradingduedate' => strtotime($work->params['sent_date']) + 86400 * 7,
+ 'allowsubmissionsfromdate' => strtotime($work->params['sent_date']),
+ 'timemodified' => time(),
+ 'grade_item_id' => 0,
+ 'files' => $files,
+ 'users' => [$adminId],
+ 'area_id' => 0
+ ];
+ }
+}
diff --git a/main/inc/lib/moodleexport/CourseExport.php b/main/inc/lib/moodleexport/CourseExport.php
new file mode 100644
index 00000000000..24393250aa2
--- /dev/null
+++ b/main/inc/lib/moodleexport/CourseExport.php
@@ -0,0 +1,325 @@
+course = $course;
+ $this->courseInfo = api_get_course_info($course->code);
+ $this->activities = $activities;
+
+ if (!$this->courseInfo) {
+ throw new Exception("Course not found.");
+ }
+ }
+
+ /**
+ * Export the course-related files to the appropriate directory.
+ */
+ public function exportCourse(string $exportDir): void
+ {
+ $courseDir = $exportDir . '/course';
+ if (!is_dir($courseDir)) {
+ mkdir($courseDir, api_get_permissions_for_new_directories(), true);
+ }
+
+ $this->createCourseXml($courseDir);
+ $this->createEnrolmentsXml($this->courseInfo['enrolments'] ?? [], $courseDir);
+ $this->createInforefXml($courseDir);
+ $this->createRolesXml($this->courseInfo['roles'] ?? [], $courseDir);
+ $this->createCalendarXml($this->courseInfo['calendar'] ?? [], $courseDir);
+ $this->createCommentsXml($this->courseInfo['comments'] ?? [], $courseDir);
+ $this->createCompetenciesXml($this->courseInfo['competencies'] ?? [], $courseDir);
+ $this->createCompletionDefaultsXml($this->courseInfo['completiondefaults'] ?? [], $courseDir);
+ $this->createContentBankXml($this->courseInfo['contentbank'] ?? [], $courseDir);
+ $this->createFiltersXml($this->courseInfo['filters'] ?? [], $courseDir);
+ }
+
+ /**
+ * Create course.xml based on the course data from MoodleExport.
+ */
+ private function createCourseXml(string $destinationDir): void
+ {
+ $courseId = $this->courseInfo['real_id'] ?? 0;
+ $contextId = $this->courseInfo['real_id'] ?? 1;
+ $shortname = $this->courseInfo['code'] ?? 'Unknown Course';
+ $fullname = $this->courseInfo['title'] ?? 'Unknown Fullname';
+ $showgrades = $this->courseInfo['showgrades'] ?? 0;
+ $startdate = $this->courseInfo['startdate'] ?? time();
+ $enddate = $this->courseInfo['enddate'] ?? time() + (60 * 60 * 24 * 365);
+ $visible = $this->courseInfo['visible'] ?? 1;
+ $enablecompletion = $this->courseInfo['enablecompletion'] ?? 0;
+
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($shortname) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($fullname) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' topics' . PHP_EOL;
+ $xmlContent .= ' ' . $showgrades . '' . PHP_EOL;
+ $xmlContent .= ' 5' . PHP_EOL;
+ $xmlContent .= ' ' . $startdate . '' . PHP_EOL;
+ $xmlContent .= ' ' . $enddate . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $visible . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . time() . '' . PHP_EOL;
+ $xmlContent .= ' ' . time() . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . $enablecompletion . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' Miscellaneous' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' topics' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' hiddensections' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' topics' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' coursedisplay' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/course.xml', $xmlContent);
+ }
+
+ /**
+ * Create enrolments.xml based on the course data from MoodleExport.
+ */
+ private function createEnrolmentsXml(array $enrolmentsData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($enrolmentsData as $enrol) {
+ $id = $enrol['id'] ?? 0;
+ $type = $enrol['type'] ?? 'manual';
+ $status = $enrol['status'] ?? 1;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($type) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $status . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/enrolments.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the inforef.xml file with file references, question categories, and role references.
+ */
+ private function createInforefXml(string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+
+ $questionCategories = [];
+ foreach ($this->activities as $activity) {
+ if ($activity['modulename'] === 'quiz') {
+ $quizExport = new QuizExport($this->course);
+ $quizData = $quizExport->getData($activity['id'], $activity['sectionid']);
+ foreach ($quizData['questions'] as $question) {
+ $categoryId = $question['questioncategoryid'];
+ if (!in_array($categoryId, $questionCategories, true)) {
+ $questionCategories[] = $categoryId;
+ }
+ }
+ }
+ }
+
+ if (!empty($questionCategories)) {
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($questionCategories as $categoryId) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $categoryId . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+
+ // Add role references
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 5' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $xmlContent .= '' . PHP_EOL;
+
+ file_put_contents($destinationDir . '/inforef.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the roles.xml file.
+ */
+ private function createRolesXml(array $rolesData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($rolesData as $role) {
+ $roleName = $role['name'] ?? 'Student';
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($roleName) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/roles.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the calendar.xml file.
+ */
+ private function createCalendarXml(array $calendarData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($calendarData as $event) {
+ $eventName = $event['name'] ?? 'Event';
+ $timestart = $event['timestart'] ?? time();
+ $duration = $event['duration'] ?? 3600;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($eventName) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $timestart . '' . PHP_EOL;
+ $xmlContent .= ' ' . $duration . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/calendar.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the comments.xml file.
+ */
+ private function createCommentsXml(array $commentsData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($commentsData as $comment) {
+ $content = $comment['content'] ?? 'No comment';
+ $author = $comment['author'] ?? 'Anonymous';
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($content) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($author) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/comments.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the competencies.xml file.
+ */
+ private function createCompetenciesXml(array $competenciesData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($competenciesData as $competency) {
+ $name = $competency['name'] ?? 'Competency';
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($name) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/competencies.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the completiondefaults.xml file.
+ */
+ private function createCompletionDefaultsXml(array $completionData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($completionData as $completion) {
+ $completionState = $completion['state'] ?? 0;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $completionState . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/completiondefaults.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the contentbank.xml file.
+ */
+ private function createContentBankXml(array $contentBankData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($contentBankData as $content) {
+ $id = $content['id'] ?? 0;
+ $name = $content['name'] ?? 'Content';
+ $xmlContent .= ' ' . htmlspecialchars($name) . '' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/contentbank.xml', $xmlContent);
+ }
+
+ /**
+ * Creates the filters.xml file.
+ */
+ private function createFiltersXml(array $filtersData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ foreach ($filtersData as $filter) {
+ $filterName = $filter['name'] ?? 'filter_example';
+ $active = $filter['active'] ?? 1;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($filterName) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $active . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= '';
+
+ file_put_contents($destinationDir . '/filters.xml', $xmlContent);
+ }
+}
diff --git a/main/inc/lib/moodleexport/FeedbackExport.php b/main/inc/lib/moodleexport/FeedbackExport.php
new file mode 100644
index 00000000000..1f7211bb78e
--- /dev/null
+++ b/main/inc/lib/moodleexport/FeedbackExport.php
@@ -0,0 +1,193 @@
+prepareActivityDirectory($exportDir, 'feedback', $moduleId);
+
+ // Get survey data from Chamilo
+ $surveyData = $this->getData($activityId, $sectionId);
+
+ // Create XML files for the survey
+ $this->createFeedbackXml($surveyData, $feedbackDir);
+ $this->createModuleXml($surveyData, $feedbackDir);
+ $this->createInforefXml($surveyData, $feedbackDir);
+ $this->createCalendarXml($surveyData, $feedbackDir);
+ $this->createCommentsXml($surveyData, $feedbackDir);
+ $this->createCompetenciesXml($surveyData, $feedbackDir);
+ $this->createCompletionXml($surveyData, $feedbackDir);
+ $this->createFiltersXml($surveyData, $feedbackDir);
+ $this->createGradeHistoryXml($surveyData, $feedbackDir);
+ $this->createGradesXml($surveyData, $feedbackDir);
+ $this->createRolesXml($surveyData, $feedbackDir);
+ }
+
+ /**
+ * Create the feedback.xml file for the Moodle feedback activity.
+ */
+ private function createFeedbackXml(array $surveyData, string $feedbackDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($surveyData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($surveyData['intro']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $surveyData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Map Chamilo questions to Moodle Feedback format
+ foreach ($surveyData['questions'] as $question) {
+ $xmlContent .= $this->createQuestionXml($question);
+ }
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('feedback', $xmlContent, $feedbackDir);
+ }
+
+ /**
+ * Create the XML for a single question in Moodle Feedback format.
+ */
+ private function createQuestionXml(array $question): string
+ {
+ $name = htmlspecialchars(strip_tags($question['text']), ENT_XML1 | ENT_QUOTES, 'UTF-8');
+ $label = htmlspecialchars(strip_tags($question['label']), ENT_XML1 | ENT_QUOTES, 'UTF-8');
+ $presentation = $this->getPresentation($question);
+ $hasValue = ($question['type'] === 'pagebreak') ? '0' : '1';
+
+ $xmlContent = '
- ' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $name . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $presentation . '' . PHP_EOL;
+ $xmlContent .= ' ' . $this->mapQuestionType($question['type']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $hasValue . '' . PHP_EOL;
+ $xmlContent .= ' ' . $question['position'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' h' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+
+ return $xmlContent;
+ }
+
+ /**
+ * Get presentation for different question types.
+ */
+ private function getPresentation(array $question): string
+ {
+ $options = array_map('strip_tags', $question['options']);
+ $sanitizedOptions = array_map(function ($option) {
+ return htmlspecialchars($option, ENT_XML1 | ENT_QUOTES, 'UTF-8');
+ }, $options);
+
+ switch ($question['type']) {
+ case 'yesno':
+ case 'multiplechoice':
+ case 'multiplechoiceother':
+ return 'r>>>>>' . implode(PHP_EOL . '|', $sanitizedOptions);
+ case 'multipleresponse':
+ return 'c>>>>>' . implode(PHP_EOL . '|', $sanitizedOptions);
+ case 'dropdown':
+ return 'd>>>>>' . implode(PHP_EOL . '|', $sanitizedOptions);
+ case 'open':
+ return '30|5'; // Textarea with rows and cols
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Map Chamilo question type to Moodle Feedback type.
+ */
+ private function mapQuestionType(string $chamiloType): string
+ {
+ $typeMap = [
+ 'yesno' => 'multichoice',
+ 'multiplechoice' => 'multichoice',
+ 'multipleresponse' => 'multichoice',
+ 'dropdown' => 'multichoice',
+ 'multiplechoiceother' => 'multichoice',
+ 'open' => 'textarea',
+ 'pagebreak' => 'pagebreak',
+ ];
+
+ return $typeMap[$chamiloType] ?? 'unknown';
+ }
+
+ /**
+ * Get survey data including questions and answers from Chamilo.
+ */
+ public function getData(int $surveyId, int $sectionId): array
+ {
+ $adminData = MoodleExport::getAdminUserData();
+ $adminId = $adminData['id'];
+
+ $survey = $this->course->resources['survey'][$surveyId];
+ $questions = [];
+ foreach ($this->course->resources['survey_question'] as $question) {
+ if ((int) $question->survey_id === $surveyId) {
+ // Debugging
+ $questions[] = [
+ 'id' => $question->id,
+ 'text' => $question->survey_question,
+ 'type' => $question->survey_question_type,
+ 'options' => array_map(function ($answer) {
+ return $answer['option_text'];
+ }, $question->answers),
+ 'position' => $question->sort,
+ 'label' => '', // Default empty label
+ ];
+ }
+ }
+
+ return [
+ 'id' => $surveyId,
+ 'moduleid' => $surveyId,
+ 'modulename' => 'feedback',
+ 'contextid' => $this->course->info['real_id'],
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 0,
+ 'name' => $survey->title,
+ 'intro' => $survey->intro,
+ 'timemodified' => time(),
+ 'questions' => $questions,
+ 'users' => [$adminId],
+ 'files' => [],
+ ];
+ }
+}
diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php
new file mode 100644
index 00000000000..dba80155c90
--- /dev/null
+++ b/main/inc/lib/moodleexport/FileExport.php
@@ -0,0 +1,308 @@
+course = $course;
+ }
+
+ /**
+ * Export files and metadata from files.xml to the specified directory.
+ */
+ public function exportFiles(array $filesData, string $exportDir): void
+ {
+ $filesDir = $exportDir . '/files';
+
+ if (!is_dir($filesDir)) {
+ mkdir($filesDir, api_get_permissions_for_new_directories(), true);
+ }
+
+ // Create placeholder index.html
+ $this->createPlaceholderFile($filesDir);
+
+ // Export each file
+ foreach ($filesData['files'] as $file) {
+ $this->copyFileToExportDir($file, $filesDir);
+ }
+
+ // Create files.xml in the export directory
+ $this->createFilesXml($filesData, $exportDir);
+ }
+
+ /**
+ * Create a placeholder index.html file to prevent an empty directory.
+ */
+ private function createPlaceholderFile(string $filesDir): void
+ {
+ $placeholderFile = $filesDir . '/index.html';
+ file_put_contents($placeholderFile, "");
+ }
+
+ /**
+ * Copy a file to the export directory using its contenthash.
+ */
+ private function copyFileToExportDir(array $file, string $filesDir): void
+ {
+ if ($file['filepath'] === '.') {
+ return;
+ }
+
+ $contenthash = $file['contenthash'];
+ $subDir = substr($contenthash, 0, 2);
+ $filePath = $this->course->path . $file['documentpath'];
+ $exportSubDir = $filesDir . '/' . $subDir;
+
+ // Ensure the subdirectory exists
+ if (!is_dir($exportSubDir)) {
+ mkdir($exportSubDir, api_get_permissions_for_new_directories(), true);
+ }
+
+ // Copy the file to the export directory
+ $destinationFile = $exportSubDir . '/' . $contenthash;
+ if (file_exists($filePath)) {
+ copy($filePath, $destinationFile);
+ } else {
+ throw new Exception("File {$filePath} not found.");
+ }
+ }
+
+ /**
+ * Create the files.xml with the provided file data.
+ */
+ private function createFilesXml(array $filesData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+
+ foreach ($filesData['files'] as $file) {
+ $xmlContent .= $this->createFileXmlEntry($file);
+ }
+
+ $xmlContent .= '' . PHP_EOL;
+ file_put_contents($destinationDir . '/files.xml', $xmlContent);
+ }
+
+ /**
+ * Create an XML entry for a file.
+ */
+ private function createFileXmlEntry(array $file): string
+ {
+ return '
' . PHP_EOL .
+ ' ' . htmlspecialchars($file['contenthash']) . '' . PHP_EOL .
+ ' ' . $file['contextid'] . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['component']) . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['filearea']) . '' . PHP_EOL .
+ ' 0' . PHP_EOL .
+ ' ' . htmlspecialchars($file['filepath']) . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['filename']) . '' . PHP_EOL .
+ ' ' . $file['userid'] . '' . PHP_EOL .
+ ' ' . $file['filesize'] . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['mimetype']) . '' . PHP_EOL .
+ ' ' . $file['status'] . '' . PHP_EOL .
+ ' ' . $file['timecreated'] . '' . PHP_EOL .
+ ' ' . $file['timemodified'] . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['source']) . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['author']) . '' . PHP_EOL .
+ ' ' . htmlspecialchars($file['license']) . '' . PHP_EOL .
+ ' 0' . PHP_EOL .
+ ' $@NULL@$' . PHP_EOL .
+ ' $@NULL@$' . PHP_EOL .
+ ' $@NULL@$' . PHP_EOL .
+ ' ' . PHP_EOL;
+ }
+
+ /**
+ * Get file data from course resources. This is for testing purposes.
+ */
+ public function getFilesData(): array
+ {
+ $adminData = MoodleExport::getAdminUserData();
+ $adminId = $adminData['id'];
+
+ $filesData = ['files' => []];
+
+ foreach ($this->course->resources[RESOURCE_DOCUMENT] as $document) {
+ $filesData = $this->processDocument($filesData, $document);
+ }
+
+ foreach ($this->course->resources[RESOURCE_WORK] as $work) {
+ $workFiles = getAllDocumentToWork($work->params['id'], $this->course->info['real_id']);
+
+ if (!empty($workFiles)) {
+ foreach ($workFiles as $file) {
+ $docData = DocumentManager::get_document_data_by_id($file['document_id'], $this->course->info['code']);
+ if (!empty($docData)) {
+ $filesData['files'][] = [
+ 'id' => $file['document_id'],
+ 'contenthash' => hash('sha1', basename($docData['path'])),
+ 'contextid' => $this->course->info['real_id'],
+ 'component' => 'mod_assign',
+ 'filearea' => 'introattachment',
+ 'itemid' => (int) $work->params['id'],
+ 'filepath' => '/',
+ 'documentpath' => 'document/'.$docData['path'],
+ 'filename' => basename($docData['path']),
+ 'userid' => $adminId,
+ 'filesize' => $docData['size'],
+ 'mimetype' => $this->getMimeType($docData['path']),
+ 'status' => 0,
+ 'timecreated' => time() - 3600,
+ 'timemodified' => time(),
+ 'source' => $docData['title'],
+ 'author' => 'Unknown',
+ 'license' => 'allrightsreserved',
+ ];
+ }
+ }
+ }
+ }
+
+ return $filesData;
+ }
+
+ /**
+ * Process a document or folder and add its data to the files array.
+ */
+ private function processDocument(array $filesData, object $document): array
+ {
+ if ($document->file_type === 'file') {
+ $filesData['files'][] = $this->getFileData($document);
+ } 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);
+ }
+ }
+ return $filesData;
+ }
+
+ /**
+ * Get file data for a single document.
+ */
+ private function getFileData(object $document): array
+ {
+ $adminData = MoodleExport::getAdminUserData();
+ $adminId = $adminData['id'];
+ $contenthash = hash('sha1', basename($document->path));
+ $mimetype = $this->getMimeType($document->path);
+
+ return [
+ 'id' => $document->source_id,
+ 'contenthash' => $contenthash,
+ 'contextid' => $document->source_id,
+ 'component' => 'mod_resource',
+ 'filearea' => 'content',
+ 'itemid' => (int) $document->source_id,
+ 'filepath' => '/',
+ 'documentpath' => $document->path,
+ 'filename' => basename($document->path),
+ 'userid' => $adminId,
+ 'filesize' => $document->size,
+ 'mimetype' => $mimetype,
+ 'status' => 0,
+ 'timecreated' => time() - 3600,
+ 'timemodified' => time(),
+ 'source' => $document->title,
+ 'author' => 'Unknown',
+ 'license' => 'allrightsreserved',
+ ];
+ }
+
+ /**
+ * Get file data for files inside a folder.
+ */
+ private function getFolderFileData(array $file, int $sourceId): 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(dirname($file['path']));
+
+ return [
+ 'id' => $file['id'],
+ 'contenthash' => $contenthash,
+ 'contextid' => $sourceId,
+ 'component' => 'mod_folder',
+ 'filearea' => 'content',
+ 'itemid' => (int) $file['id'],
+ 'filepath' => $filepath,
+ 'documentpath' => 'document/'.$file['path'],
+ 'filename' => $filename,
+ 'userid' => $adminId,
+ 'filesize' => $file['size'],
+ 'mimetype' => $mimetype,
+ 'status' => 0,
+ 'timecreated' => time() - 3600,
+ 'timemodified' => time(),
+ 'source' => $file['title'],
+ 'author' => 'Unknown',
+ 'license' => 'allrightsreserved',
+ ];
+ }
+
+ /**
+ * Ensure the directory path has a trailing slash.
+ */
+ private function ensureTrailingSlash($path): string
+ {
+ return empty($path) || $path === '.' || $path === '/' ? '/' : rtrim($path, '/') . '/';
+ }
+
+ /**
+ * Get MIME type based on the file extension.
+ */
+ private function getMimeType($filePath): string
+ {
+ $extension = pathinfo($filePath, PATHINFO_EXTENSION);
+ $mimeTypes = $this->getMimeTypes();
+
+ return $mimeTypes[$extension] ?? 'application/octet-stream';
+ }
+
+ /**
+ * Get an array of file extensions and their corresponding MIME types.
+ */
+ private function getMimeTypes(): array
+ {
+ return [
+ 'pdf' => 'application/pdf',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'gif' => 'image/gif',
+ 'html' => 'text/html',
+ 'txt' => 'text/plain',
+ 'doc' => 'application/msword',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'xls' => 'application/vnd.ms-excel',
+ 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'zip' => 'application/zip',
+ 'rar' => 'application/x-rar-compressed',
+ ];
+ }
+}
diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php
new file mode 100644
index 00000000000..e8a0814058c
--- /dev/null
+++ b/main/inc/lib/moodleexport/FolderExport.php
@@ -0,0 +1,124 @@
+prepareActivityDirectory($exportDir, 'folder', $moduleId);
+
+ // Retrieve folder data
+ $folderData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files
+ $this->createFolderXml($folderData, $folderDir);
+ $this->createModuleXml($folderData, $folderDir);
+ $this->createGradesXml($folderData, $folderDir);
+ $this->createFiltersXml($folderData, $folderDir);
+ $this->createGradeHistoryXml($folderData, $folderDir);
+ $this->createInforefXml($this->getFilesForFolder($activityId), $folderDir);
+ $this->createRolesXml($folderData, $folderDir);
+ $this->createCommentsXml($folderData, $folderDir);
+ $this->createCalendarXml($folderData, $folderDir);
+ }
+
+ /**
+ * Create the XML file for the folder.
+ */
+ private function createFolderXml(array $folderData, string $folderDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($folderData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . $folderData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('folder', $xmlContent, $folderDir);
+ }
+
+ /**
+ * Get folder data dynamically from the course.
+ */
+ public function getData(int $folderId, int $sectionId): array
+ {
+ $folder = $this->course->resources['document'][$folderId];
+
+ return [
+ 'id' => $folderId,
+ 'moduleid' => $folder->source_id,
+ 'modulename' => 'folder',
+ 'contextid' => $folder->source_id,
+ 'name' => $folder->title,
+ 'sectionid' => $sectionId,
+ 'timemodified' => time(),
+ ];
+ }
+
+ /**
+ * Get the list of files for a folder.
+ */
+ private function getFilesForFolder(int $folderId): array
+ {
+ $documentData = \DocumentManager::getAllDocumentsByParentId($this->course->info, $folderId);
+
+ $files = [];
+ foreach ($documentData as $doc) {
+ if ($doc['filetype'] === 'file') {
+ $files[] = [
+ 'id' => (int) $doc['id'],
+ 'contenthash' => 'hash' . $doc['id'],
+ 'filename' => $doc['basename'],
+ 'filepath' => $doc['path'],
+ 'filesize' => (int) $doc['size'],
+ 'mimetype' => $this->getMimeType($doc['basename']),
+ ];
+ }
+ }
+
+ return ['files' => $files];
+ }
+
+ /**
+ * Get the MIME type for a given file.
+ */
+ private function getMimeType(string $filename): string
+ {
+ $ext = pathinfo($filename, PATHINFO_EXTENSION);
+ $mimetypes = [
+ 'pdf' => 'application/pdf',
+ 'png' => 'image/png',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'doc' => 'application/msword',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ ];
+
+ return $mimetypes[$ext] ?? 'application/octet-stream';
+ }
+}
diff --git a/main/inc/lib/moodleexport/ForumExport.php b/main/inc/lib/moodleexport/ForumExport.php
new file mode 100644
index 00000000000..82462332ef4
--- /dev/null
+++ b/main/inc/lib/moodleexport/ForumExport.php
@@ -0,0 +1,188 @@
+prepareActivityDirectory($exportDir, 'forum', $moduleId);
+
+ // Retrieve forum data
+ $forumData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files for the forum
+ $this->createForumXml($forumData, $forumDir);
+ $this->createModuleXml($forumData, $forumDir);
+ $this->createGradesXml($forumData, $forumDir);
+ $this->createGradeHistoryXml($forumData, $forumDir);
+ $this->createInforefXml($forumData, $forumDir);
+ $this->createRolesXml($forumData, $forumDir);
+ $this->createCalendarXml($forumData, $forumDir);
+ $this->createCommentsXml($forumData, $forumDir);
+ $this->createCompetenciesXml($forumData, $forumDir);
+ $this->createFiltersXml($forumData, $forumDir);
+ }
+
+ /**
+ * Create the forum.xml file with all forum data.
+ */
+ private function createForumXml(array $forumData, string $forumDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' general' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($forumData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($forumData['description']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 100' . PHP_EOL;
+ $xmlContent .= ' 512000' . PHP_EOL;
+ $xmlContent .= ' 9' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $forumData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+
+ // Add forum threads
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($forumData['threads'] as $thread) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($thread['title']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $thread['firstpost'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $thread['userid'] . '' . PHP_EOL;
+ $xmlContent .= ' -1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $thread['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $thread['usermodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+
+ // Add forum posts to the thread
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($thread['posts'] as $post) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $post['parent'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $post['userid'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $post['created'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $post['modified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $post['mailed'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($post['subject']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($post['message']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $thread['userid'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $thread['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('forum', $xmlContent, $forumDir);
+ }
+
+ /**
+ * Get all forum data from the course.
+ */
+ public function getData(int $forumId, int $sectionId): ?array
+ {
+ $forum = $this->course->resources['forum'][$forumId]->obj;
+
+ $adminData = MoodleExport::getAdminUserData();
+ $adminId = $adminData['id'];
+
+ $threads = [];
+ foreach ($this->course->resources['thread'] as $threadId => $thread) {
+ if ($thread->obj->forum_id == $forumId) {
+ // Get the posts for each thread
+ $posts = [];
+ foreach ($this->course->resources['post'] as $postId => $post) {
+ if ($post->obj->thread_id == $threadId) {
+ $posts[] = [
+ 'id' => $post->obj->post_id,
+ 'userid' => $adminId,
+ 'message' => $post->obj->post_text,
+ 'created' => strtotime($post->obj->post_date),
+ 'modified' => strtotime($post->obj->post_date),
+ ];
+ }
+ }
+
+ $threads[] = [
+ 'id' => $thread->obj->thread_id,
+ 'title' => $thread->obj->thread_title,
+ 'userid' => $adminId,
+ 'timemodified' => strtotime($thread->obj->thread_date),
+ 'usermodified' => $adminId,
+ 'posts' => $posts,
+ ];
+ }
+ }
+
+ $fileIds = [];
+
+ return [
+ 'id' => $forumId,
+ 'moduleid' => $forumId,
+ 'modulename' => 'forum',
+ 'contextid' => $this->course->info['real_id'],
+ 'name' => $forum->forum_title,
+ 'description' => $forum->forum_comment,
+ 'timecreated' => time(),
+ 'timemodified' => time(),
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 1,
+ 'userid' => $adminId,
+ 'threads' => $threads,
+ 'users' => [$adminId],
+ 'files' => $fileIds,
+ ];
+ }
+}
diff --git a/main/inc/lib/moodleexport/GlossaryExport.php b/main/inc/lib/moodleexport/GlossaryExport.php
new file mode 100644
index 00000000000..577b2b6740a
--- /dev/null
+++ b/main/inc/lib/moodleexport/GlossaryExport.php
@@ -0,0 +1,146 @@
+prepareActivityDirectory($exportDir, 'glossary', $moduleId);
+
+ // Retrieve glossary data
+ $glossaryData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files for the glossary
+ $this->createGlossaryXml($glossaryData, $glossaryDir);
+ $this->createModuleXml($glossaryData, $glossaryDir);
+ $this->createGradesXml($glossaryData, $glossaryDir);
+ $this->createGradeHistoryXml($glossaryData, $glossaryDir);
+ $this->createInforefXml($glossaryData, $glossaryDir);
+ $this->createRolesXml($glossaryData, $glossaryDir);
+ $this->createCalendarXml($glossaryData, $glossaryDir);
+ $this->createCommentsXml($glossaryData, $glossaryDir);
+ $this->createCompetenciesXml($glossaryData, $glossaryDir);
+ $this->createFiltersXml($glossaryData, $glossaryDir);
+ }
+
+ /**
+ * Create the XML file for the glossary with all terms combined.
+ */
+ private function createGlossaryXml(array $glossaryData, string $glossaryDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($glossaryData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' dictionary' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 10' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 100' . PHP_EOL;
+ $xmlContent .= ' ' . $glossaryData['timecreated'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $glossaryData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Add glossary terms (entries)
+ foreach ($glossaryData['entries'] as $entry) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $entry['userid'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($entry['concept']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $entry['timecreated'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $entry['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('glossary', $xmlContent, $glossaryDir);
+ }
+
+ /**
+ * Get all terms from the course and group them into a single glossary.
+ */
+ public function getData(int $glossaryId, int $sectionId): ?array
+ {
+ $adminData = MoodleExport::getAdminUserData();
+ $adminId = $adminData['id'];
+
+ $glossaryEntries = [];
+ foreach ($this->course->resources['glossary'] as $glossary) {
+ $glossaryEntries[] = [
+ 'id' => $glossary->glossary_id,
+ 'userid' => $adminId,
+ 'concept' => $glossary->name,
+ 'definition' => $glossary->description,
+ 'timecreated' => time(),
+ 'timemodified' => time(),
+ ];
+ }
+
+ // Return the glossary data with all terms included
+ return [
+ 'id' => $glossaryId,
+ 'moduleid' => $glossaryId,
+ 'modulename' => 'glossary',
+ 'contextid' => $this->course->info['real_id'],
+ 'name' => get_lang('Glossary'),
+ 'description' => '',
+ 'timecreated' => time(),
+ 'timemodified' => time(),
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 0,
+ 'userid' => $adminId,
+ 'entries' => $glossaryEntries,
+ 'users' => [$adminId],
+ 'files' => [],
+ ];
+ }
+}
diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php
new file mode 100644
index 00000000000..825f5595125
--- /dev/null
+++ b/main/inc/lib/moodleexport/MoodleExport.php
@@ -0,0 +1,754 @@
+course = $course;
+ }
+
+ /**
+ * Export the Moodle course in .mbz format.
+ */
+ public function export(string $courseId, string $exportDir, int $version)
+ {
+ $tempDir = api_get_path(SYS_ARCHIVE_PATH) . $exportDir;
+
+ if (!is_dir($tempDir)) {
+ if (!mkdir($tempDir, api_get_permissions_for_new_directories(), true)) {
+ throw new Exception(get_lang('ErrorCreatingDirectory'));
+ }
+ }
+
+ $courseInfo = api_get_course_info($courseId);
+ if (!$courseInfo) {
+ throw new Exception(get_lang('CourseNotFound'));
+ }
+
+ // Generate the moodle_backup.xml
+ $this->createMoodleBackupXml($tempDir, $version);
+
+ // Get the activities from the course
+ $activities = $this->getActivities();
+
+ // Export course-related files
+ $courseExport = new CourseExport($this->course, $activities);
+ $courseExport->exportCourse($tempDir);
+
+ // Export files-related data and actual files
+ $fileExport = new FileExport($this->course);
+ $filesData = $fileExport->getFilesData();
+ $fileExport->exportFiles($filesData, $tempDir);
+
+ // Export sections of the course
+ $this->exportSections($tempDir);
+
+ // Export all root XML files
+ $this->exportRootXmlFiles($tempDir);
+
+ // Compress everything into a .mbz (ZIP) file
+ $exportedFile = $this->createMbzFile($tempDir);
+
+ // Clean up temporary directory
+ $this->cleanupTempDir($tempDir);
+
+ return $exportedFile;
+ }
+
+ /**
+ * Export root XML files such as badges, completion, gradebook, etc.
+ */
+ private function exportRootXmlFiles(string $exportDir): void
+ {
+ $this->exportBadgesXml($exportDir);
+ $this->exportCompletionXml($exportDir);
+ $this->exportGradebookXml($exportDir);
+ $this->exportGradeHistoryXml($exportDir);
+ $this->exportGroupsXml($exportDir);
+ $this->exportOutcomesXml($exportDir);
+
+ // Export quizzes and their questions
+ $activities = $this->getActivities();
+ $questionsData = [];
+ foreach ($activities as $activity) {
+ if ($activity['modulename'] === 'quiz') {
+ $quizExport = new QuizExport($this->course);
+ $quizData = $quizExport->getData($activity['id'], $activity['sectionid']);
+ $questionsData[] = $quizData;
+ }
+ }
+ $this->exportQuestionsXml($questionsData, $exportDir);
+
+ $this->exportRolesXml($exportDir);
+ $this->exportScalesXml($exportDir);
+ $this->exportUsersXml($exportDir);
+ }
+
+ /**
+ * Create the moodle_backup.xml file with the required course details.
+ */
+ private function createMoodleBackupXml(string $destinationDir, int $version): void
+ {
+ // Generate course information and backup metadata
+ $courseInfo = api_get_course_info($this->course->code);
+ $backupId = md5(uniqid(mt_rand(), true));
+ $siteHash = md5(uniqid(mt_rand(), true));
+ $wwwRoot = api_get_path(WEB_PATH);
+
+ // Build the XML content for the backup
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $xmlContent .= ' backup-' . htmlspecialchars($courseInfo['code']) . '.mbz' . PHP_EOL;
+ $xmlContent .= ' ' . ($version === 3 ? '2021051718' : '2022041900') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($version === 3 ? '3.11.18 (Build: 20231211)' : '4.x version here') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($version === 3 ? '2021051700' : '2022041900') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($version === 3 ? '3.11' : '4.x') . '' . PHP_EOL;
+ $xmlContent .= ' ' . time() . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $wwwRoot . '' . PHP_EOL;
+ $xmlContent .= ' ' . $siteHash . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($courseInfo['real_id']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . get_lang('Topics') . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($courseInfo['title']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($courseInfo['code']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $courseInfo['startdate'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $courseInfo['enddate'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $courseInfo['real_id'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . api_get_current_access_url_id() . '' . PHP_EOL;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' course' . PHP_EOL;
+ $xmlContent .= ' moodle2' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 10' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Contents with activities and sections
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Export sections dynamically and add them to the XML
+ $sections = $this->getSections();
+ if (!empty($sections)) {
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($sections as $section) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $section['id'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($section['name']) . '' . PHP_EOL;
+ $xmlContent .= ' sections/section_' . $section['id'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+
+ $activities = $this->getActivities();
+ if (!empty($activities)) {
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($activities as $activity) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $activity['moduleid'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $activity['sectionid'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($activity['modulename']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($activity['title']) . '' . PHP_EOL;
+ $xmlContent .= ' activities/' . $activity['modulename'] . '_' . $activity['moduleid'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+
+ // Course directory
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $courseInfo['real_id'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($courseInfo['title']) . '' . PHP_EOL;
+ $xmlContent .= ' course' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Backup settings
+ $xmlContent .= ' ' . PHP_EOL;
+ $settings = $this->exportBackupSettings($sections, $activities);
+ foreach ($settings as $setting) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($setting['level']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($setting['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $setting['value'] . '' . PHP_EOL;
+ if (isset($setting['section'])) {
+ $xmlContent .= ' ' . htmlspecialchars($setting['section']) . '' . PHP_EOL;
+ }
+ if (isset($setting['activity'])) {
+ $xmlContent .= ' ' . htmlspecialchars($setting['activity']) . '' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $xmlFile = $destinationDir . '/moodle_backup.xml';
+ file_put_contents($xmlFile, $xmlContent);
+ }
+
+ /**
+ * Get all sections from the course.
+ */
+ private function getSections(): array
+ {
+ $sectionExport = new SectionExport($this->course);
+ $sections = [];
+
+ foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
+ if ($learnpath->lp_type == '1') {
+ $sections[] = $sectionExport->getSectionData($learnpath);
+ }
+ }
+
+ // Add a general section for resources without a lesson
+ $sections[] = [
+ 'id' => 0,
+ 'number' => 0,
+ 'name' => get_lang('General'),
+ 'summary' => get_lang('GeneralResourcesCourse'),
+ 'sequence' => 0,
+ 'visible' => 1,
+ 'timemodified' => time(),
+ 'activities' => $sectionExport->getActivitiesForGeneral(),
+ ];
+
+ return $sections;
+ }
+
+ /**
+ * Get all activities from the course.
+ */
+ private function getActivities(): array
+ {
+ $activities = [];
+ $glossaryAdded = false;
+
+ foreach ($this->course->resources as $resourceType => $resources) {
+ foreach ($resources as $resource) {
+ $exportClass = null;
+ $moduleName = '';
+ $title = '';
+ $id = 0;
+
+ // Handle quizzes
+ if ($resourceType === RESOURCE_QUIZ && $resource->obj->iid > 0) {
+ $exportClass = QuizExport::class;
+ $moduleName = 'quiz';
+ $id = $resource->obj->iid;
+ $title = $resource->obj->title;
+ }
+ // Handle links
+ if ($resourceType === RESOURCE_LINK && $resource->source_id > 0) {
+ $exportClass = UrlExport::class;
+ $moduleName = 'url';
+ $id = $resource->source_id;
+ $title = $resource->title;
+ }
+ // Handle glossaries
+ elseif ($resourceType === RESOURCE_GLOSSARY && $resource->glossary_id > 0 && !$glossaryAdded) {
+ $exportClass = GlossaryExport::class;
+ $moduleName = 'glossary';
+ $id = 1;
+ $title = get_lang('Glossary');
+ $glossaryAdded = true;
+ }
+ // Handle forums
+ elseif ($resourceType === RESOURCE_FORUM && $resource->source_id > 0) {
+ $exportClass = ForumExport::class;
+ $moduleName = 'forum';
+ $id = $resource->obj->iid;
+ $title = $resource->obj->forum_title;
+ }
+ // Handle documents (HTML pages)
+ 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)) {
+ $exportClass = PageExport::class;
+ $moduleName = 'page';
+ $id = $resource->source_id;
+ $title = $document['title'];
+ } elseif ('file' === $resource->file_type) {
+ $exportClass = ResourceExport::class;
+ $moduleName = 'resource';
+ $id = $resource->source_id;
+ $title = $resource->title;
+ } elseif ('folder' === $resource->file_type) {
+ $exportClass = FolderExport::class;
+ $moduleName = 'folder';
+ $id = $resource->source_id;
+ $title = $resource->title;
+ }
+ }
+ // Handle assignments (work)
+ elseif ($resourceType === RESOURCE_WORK && $resource->source_id > 0) {
+ $exportClass = AssignExport::class;
+ $moduleName = 'assign';
+ $id = $resource->source_id;
+ $title = $resource->params['title'] ?? '';
+ }
+ // Handle feedback (survey)
+ elseif ($resourceType === RESOURCE_SURVEY && $resource->source_id > 0) {
+ $exportClass = FeedbackExport::class;
+ $moduleName = 'feedback';
+ $id = $resource->source_id;
+ $title = $resource->params['title'] ?? '';
+ }
+
+ // Add the activity if the class and module name are set
+ if ($exportClass && $moduleName) {
+ $exportInstance = new $exportClass($this->course);
+ $activities[] = [
+ 'id' => $id,
+ 'sectionid' => $exportInstance->getSectionIdForActivity($id, $resourceType),
+ 'modulename' => $moduleName,
+ 'moduleid' => $id,
+ 'title' => $title,
+ ];
+ }
+ }
+ }
+
+ return $activities;
+ }
+
+ /**
+ * Export the sections of the course.
+ */
+ private function exportSections(string $exportDir): void
+ {
+ $sections = $this->getSections();
+
+ foreach ($sections as $section) {
+ $sectionExport = new SectionExport($this->course);
+ $sectionExport->exportSection($section['id'], $exportDir);
+ }
+ }
+
+ /**
+ * Create a .mbz (ZIP) file from the exported data.
+ */
+ private function createMbzFile(string $sourceDir): string
+ {
+ $zip = new ZipArchive();
+ $zipFile = $sourceDir . '.mbz';
+
+ if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
+ throw new Exception(get_lang('ErrorCreatingZip'));
+ }
+
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($sourceDir),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($files as $file) {
+ if (!$file->isDir()) {
+ $filePath = $file->getRealPath();
+ $relativePath = substr($filePath, strlen($sourceDir) + 1);
+
+ if (!$zip->addFile($filePath, $relativePath)) {
+ throw new Exception(get_lang('ErrorAddingFileToZip') . ": $relativePath");
+ }
+ }
+ }
+
+ if (!$zip->close()) {
+ throw new Exception(get_lang('ErrorClosingZip'));
+ }
+
+ return $zipFile;
+ }
+
+ /**
+ * Clean up the temporary directory used for export.
+ */
+ private function cleanupTempDir(string $dir): void
+ {
+ $this->recursiveDelete($dir);
+ }
+
+ /**
+ * Recursively delete a directory and its contents.
+ */
+ private function recursiveDelete(string $dir): void
+ {
+ $files = array_diff(scandir($dir), ['.', '..']);
+ foreach ($files as $file) {
+ $path = "$dir/$file";
+ is_dir($path) ? $this->recursiveDelete($path) : unlink($path);
+ }
+ rmdir($dir);
+ }
+
+ /**
+ * Export badges data to XML file.
+ */
+ private function exportBadgesXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/badges.xml', $xmlContent);
+ }
+
+ /**
+ * Export course completion data to XML file.
+ */
+ private function exportCompletionXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/completion.xml', $xmlContent);
+ }
+
+ /**
+ * Export gradebook data to XML file.
+ */
+ private function exportGradebookXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/gradebook.xml', $xmlContent);
+ }
+
+ /**
+ * Export grade history data to XML file.
+ */
+ private function exportGradeHistoryXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/grade_history.xml', $xmlContent);
+ }
+
+ /**
+ * Export groups data to XML file.
+ */
+ private function exportGroupsXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/groups.xml', $xmlContent);
+ }
+
+ /**
+ * Export outcomes data to XML file.
+ */
+ private function exportOutcomesXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/outcomes.xml', $xmlContent);
+ }
+
+ /**
+ * Export questions data to XML file.
+ */
+ public function exportQuestionsXml(array $questionsData, string $exportDir): void
+ {
+ $quizExport = new QuizExport($this->course);
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+
+ foreach ($questionsData as $quiz) {
+ $categoryId = $quiz['questions'][0]['questioncategoryid'] ?? '0';
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' Default for ' . htmlspecialchars($quiz['name'] ?? 'Unknown') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quiz['contextid'] ?? '0') . '' . PHP_EOL;
+ $xmlContent .= ' 70' . PHP_EOL;
+ $xmlContent .= ' ' . ($quiz['moduleid'] ?? '0') . '' . PHP_EOL;
+ $xmlContent .= ' The default category for questions shared in context "' . htmlspecialchars($quiz['name'] ?? 'Unknown') . '".' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' moodle+' . time() . '+CATEGORYSTAMP' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 999' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ foreach ($quiz['questions'] as $question) {
+ $xmlContent .= $quizExport->exportQuestion($question);
+ }
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/questions.xml', $xmlContent);
+ }
+
+ /**
+ * Export roles data to XML file.
+ */
+ private function exportRolesXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' student' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 5' . PHP_EOL;
+ $xmlContent .= ' student' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '' . PHP_EOL;
+
+ file_put_contents($exportDir . '/roles.xml', $xmlContent);
+ }
+
+ /**
+ * Export scales data to XML file.
+ */
+ private function exportScalesXml(string $exportDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= '';
+ file_put_contents($exportDir . '/scales.xml', $xmlContent);
+ }
+
+ /**
+ * Sets the admin user data.
+ */
+ public function setAdminUserData(int $id, string $username, string $email): void
+ {
+ self::$adminUserData = [
+ 'id' => $id,
+ 'contextid' => $id,
+ 'username' => $username,
+ 'idnumber' => '',
+ 'email' => $email,
+ 'phone1' => '',
+ 'phone2' => '',
+ 'institution' => '',
+ 'department' => '',
+ 'address' => '',
+ 'city' => 'London',
+ 'country' => 'GB',
+ 'lastip' => '127.0.0.1',
+ 'picture' => '0',
+ 'description' => '',
+ 'descriptionformat' => 1,
+ 'imagealt' => '$@NULL@$',
+ 'auth' => 'manual',
+ 'firstname' => 'Admin',
+ 'lastname' => 'User',
+ 'confirmed' => 1,
+ 'policyagreed' => 0,
+ 'deleted' => 0,
+ 'lang' => 'en',
+ 'theme' => '',
+ 'timezone' => 99,
+ 'firstaccess' => time(),
+ 'lastaccess' => time() - (60 * 60 * 24 * 7),
+ 'lastlogin' => time() - (60 * 60 * 24 * 2),
+ 'currentlogin' => time(),
+ 'mailformat' => 1,
+ 'maildigest' => 0,
+ 'maildisplay' => 1,
+ 'autosubscribe' => 1,
+ 'trackforums' => 0,
+ 'timecreated' => time(),
+ 'timemodified' => time(),
+ 'trustbitmask' => 0,
+ 'preferences' => [
+ ['name' => 'core_message_migrate_data', 'value' => 1],
+ ['name' => 'auth_manual_passwordupdatetime', 'value' => time()],
+ ['name' => 'email_bounce_count', 'value' => 1],
+ ['name' => 'email_send_count', 'value' => 1],
+ ['name' => 'login_failed_count_since_success', 'value' => 0],
+ ['name' => 'filepicker_recentrepository', 'value' => 5],
+ ['name' => 'filepicker_recentlicense', 'value' => 'unknown'],
+ ],
+ ];
+ }
+
+ /**
+ * Returns hardcoded data for the admin user.
+ *
+ * @return array
+ */
+ public static function getAdminUserData(): array
+ {
+ return self::$adminUserData;
+ }
+
+ /**
+ * Export the user XML with admin user data.
+ */
+ private function exportUsersXml(string $exportDir): void
+ {
+ $adminData = self::getAdminUserData();
+
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['username'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['idnumber'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['email'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['phone1'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['phone2'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['institution'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['department'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['address'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['city'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['country'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['lastip'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['picture'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['description'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['descriptionformat'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['imagealt'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['auth'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['firstname'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['lastname'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['confirmed'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['policyagreed'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['deleted'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['lang'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['theme'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['timezone'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['firstaccess'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['lastaccess'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['lastlogin'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['currentlogin'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['mailformat'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['maildigest'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['maildisplay'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['autosubscribe'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['trackforums'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['timecreated'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $adminData['trustbitmask'] . '' . PHP_EOL;
+
+ // Preferences
+ if (isset($adminData['preferences']) && is_array($adminData['preferences'])) {
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($adminData['preferences'] as $preference) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($preference['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($preference['value']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ } else {
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+
+ // Roles (empty for now)
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ // Save the content to the users.xml file
+ file_put_contents($exportDir . '/users.xml', $xmlContent);
+ }
+
+ /**
+ * Export the backup settings, including dynamic settings for sections and activities.
+ */
+ private function exportBackupSettings(array $sections, array $activities): array
+ {
+ // root-level settings
+ $settings = [
+ ['level' => 'root', 'name' => 'filename', 'value' => 'backup-moodle-course-' . time() . '.mbz'],
+ ['level' => 'root', 'name' => 'imscc11', 'value' => '0'],
+ ['level' => 'root', 'name' => 'users', 'value' => '1'],
+ ['level' => 'root', 'name' => 'anonymize', 'value' => '0'],
+ ['level' => 'root', 'name' => 'role_assignments', 'value' => '1'],
+ ['level' => 'root', 'name' => 'activities', 'value' => '1'],
+ ['level' => 'root', 'name' => 'blocks', 'value' => '1'],
+ ['level' => 'root', 'name' => 'files', 'value' => '1'],
+ ['level' => 'root', 'name' => 'filters', 'value' => '1'],
+ ['level' => 'root', 'name' => 'comments', 'value' => '1'],
+ ['level' => 'root', 'name' => 'badges', 'value' => '1'],
+ ['level' => 'root', 'name' => 'calendarevents', 'value' => '1'],
+ ['level' => 'root', 'name' => 'userscompletion', 'value' => '1'],
+ ['level' => 'root', 'name' => 'logs', 'value' => '0'],
+ ['level' => 'root', 'name' => 'grade_histories', 'value' => '0'],
+ ['level' => 'root', 'name' => 'questionbank', 'value' => '1'],
+ ['level' => 'root', 'name' => 'groups', 'value' => '1'],
+ ['level' => 'root', 'name' => 'competencies', 'value' => '0'],
+ ['level' => 'root', 'name' => 'customfield', 'value' => '1'],
+ ['level' => 'root', 'name' => 'contentbankcontent', 'value' => '1'],
+ ['level' => 'root', 'name' => 'legacyfiles', 'value' => '1'],
+ ];
+
+ // section-level settings
+ foreach ($sections as $section) {
+ $settings[] = [
+ 'level' => 'section',
+ 'section' => 'section_' . $section['id'],
+ 'name' => 'section_' . $section['id'] . '_included',
+ 'value' => '1',
+ ];
+ $settings[] = [
+ 'level' => 'section',
+ 'section' => 'section_' . $section['id'],
+ 'name' => 'section_' . $section['id'] . '_userinfo',
+ 'value' => '1',
+ ];
+ }
+
+ // activity-level settings
+ foreach ($activities as $activity) {
+ $settings[] = [
+ 'level' => 'activity',
+ 'activity' => $activity['modulename'] . '_' . $activity['moduleid'],
+ 'name' => $activity['modulename'] . '_' . $activity['moduleid'] . '_included',
+ 'value' => '1',
+ ];
+ $settings[] = [
+ 'level' => 'activity',
+ 'activity' => $activity['modulename'] . '_' . $activity['moduleid'],
+ 'name' => $activity['modulename'] . '_' . $activity['moduleid'] . '_userinfo',
+ 'value' => '1',
+ ];
+ }
+
+ return $settings;
+ }
+}
diff --git a/main/inc/lib/moodleexport/PageExport.php b/main/inc/lib/moodleexport/PageExport.php
new file mode 100644
index 00000000000..e91b46d8fee
--- /dev/null
+++ b/main/inc/lib/moodleexport/PageExport.php
@@ -0,0 +1,109 @@
+prepareActivityDirectory($exportDir, 'page', $moduleId);
+
+ // Retrieve page data
+ $pageData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files
+ $this->createPageXml($pageData, $pageDir);
+ $this->createModuleXml($pageData, $pageDir);
+ $this->createGradesXml($pageData, $pageDir);
+ $this->createFiltersXml($pageData, $pageDir);
+ $this->createGradeHistoryXml($pageData, $pageDir);
+ $this->createInforefXml($pageData, $pageDir);
+ $this->createRolesXml($pageData, $pageDir);
+ $this->createCommentsXml($pageData, $pageDir);
+ $this->createCalendarXml($pageData, $pageDir);
+ }
+
+ /**
+ * Create the XML file for the page.
+ */
+ private function createPageXml(array $pageData, string $pageDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($pageData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($pageData['intro']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($pageData['content']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 5' . PHP_EOL;
+ $xmlContent .= ' a:3:{s:12:"printheading";s:1:"1";s:10:"printintro";s:1:"0";s:17:"printlastmodified";s:1:"1";}' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . $pageData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('page', $xmlContent, $pageDir);
+ }
+
+ /**
+ * Get page data dynamically from the course.
+ */
+ public function getData(int $pageId, int $sectionId): ?array
+ {
+ $pageResources = $this->course->resources[RESOURCE_DOCUMENT];
+
+ foreach ($pageResources as $page) {
+ if ($page->source_id == $pageId) {
+ $contextid = $this->course->info['real_id'];
+
+ return [
+ 'id' => $page->source_id,
+ 'moduleid' => $page->source_id,
+ 'modulename' => 'page',
+ 'contextid' => $contextid,
+ 'name' => $page->title,
+ 'intro' => $page->comment ?? '',
+ 'content' => $this->getPageContent($page),
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 1,
+ 'display' => 0,
+ 'timemodified' => time(),
+ 'users' => [],
+ 'files' => [],
+ ];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves the content of the page.
+ */
+ private function getPageContent(object $page): string
+ {
+ if ($page->file_type === 'file') {
+ return file_get_contents($this->course->path . $page->path);
+ }
+
+ return '';
+ }
+}
diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php
new file mode 100644
index 00000000000..a2261945936
--- /dev/null
+++ b/main/inc/lib/moodleexport/QuizExport.php
@@ -0,0 +1,487 @@
+prepareActivityDirectory($exportDir, 'quiz', $moduleId);
+
+ // Retrieve quiz data
+ $quizData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files
+ $this->createQuizXml($quizData, $quizDir);
+ $this->createModuleXml($quizData, $quizDir);
+ $this->createGradesXml($quizData, $quizDir);
+ $this->createCompletionXml($quizData, $quizDir);
+ $this->createCommentsXml($quizData, $quizDir);
+ $this->createCompetenciesXml($quizData, $quizDir);
+ $this->createFiltersXml($quizData, $quizDir);
+ $this->createGradeHistoryXml($quizData, $quizDir);
+ $this->createInforefXml($quizData, $quizDir);
+ $this->createRolesXml($quizData, $quizDir);
+ $this->createCalendarXml($quizData, $quizDir);
+ }
+
+ /**
+ * Retrieves the quiz data.
+ */
+ public function getData(int $quizId, int $sectionId): array
+ {
+ $quizResources = $this->course->resources[RESOURCE_QUIZ];
+
+ foreach ($quizResources as $quiz) {
+ if ($quiz->obj->iid == $quizId) {
+ $contextid = $quiz->obj->c_id;
+
+ return [
+ 'id' => $quiz->obj->iid,
+ 'name' => $quiz->obj->title,
+ 'intro' => $quiz->obj->description,
+ 'timeopen' => $quiz->obj->start_time ?? 0,
+ 'timeclose' => $quiz->obj->end_time ?? 0,
+ 'timelimit' => $quiz->obj->timelimit ?? 0,
+ 'grademethod' => $quiz->obj->grademethod ?? 1,
+ 'decimalpoints' => $quiz->obj->decimalpoints ?? 2,
+ 'sumgrades' => $quiz->obj->sumgrades ?? 0,
+ 'grade' => $quiz->obj->grade ?? 0,
+ 'questionsperpage' => $quiz->obj->questionsperpage ?? 1,
+ 'preferredbehaviour' => $quiz->obj->preferredbehaviour ?? 'deferredfeedback',
+ 'shuffleanswers' => $quiz->obj->shuffleanswers ?? 1,
+ 'questions' => $this->getQuestionsForQuiz($quizId),
+ 'feedbacks' => $this->getFeedbacksForQuiz($quizId),
+ 'sectionid' => $sectionId,
+ 'moduleid' => $quiz->obj->iid ?? 0,
+ 'modulename' => 'quiz',
+ 'contextid' => $contextid,
+ 'overduehandling' => $quiz->obj->overduehandling ?? 'autosubmit',
+ 'graceperiod' => $quiz->obj->graceperiod ?? 0,
+ 'canredoquestions' => $quiz->obj->canredoquestions ?? 0,
+ 'attempts_number' => $quiz->obj->attempts_number ?? 0,
+ 'attemptonlast' => $quiz->obj->attemptonlast ?? 0,
+ 'questiondecimalpoints' => $quiz->obj->questiondecimalpoints ?? 2,
+ 'reviewattempt' => $quiz->obj->reviewattempt ?? 0,
+ 'reviewcorrectness' => $quiz->obj->reviewcorrectness ?? 0,
+ 'reviewmarks' => $quiz->obj->reviewmarks ?? 0,
+ 'reviewspecificfeedback' => $quiz->obj->reviewspecificfeedback ?? 0,
+ 'reviewgeneralfeedback' => $quiz->obj->reviewgeneralfeedback ?? 0,
+ 'reviewrightanswer' => $quiz->obj->reviewrightanswer ?? 0,
+ 'reviewoverallfeedback' => $quiz->obj->reviewoverallfeedback ?? 0,
+ 'timecreated' => $quiz->obj->insert_date ?? time(),
+ 'timemodified' => $quiz->obj->lastedit_date ?? time(),
+ 'password' => $quiz->obj->password ?? '',
+ 'subnet' => $quiz->obj->subnet ?? '',
+ 'browsersecurity' => $quiz->obj->browsersecurity ?? '-',
+ 'delay1' => $quiz->obj->delay1 ?? 0,
+ 'delay2' => $quiz->obj->delay2 ?? 0,
+ 'showuserpicture' => $quiz->obj->showuserpicture ?? 0,
+ 'showblocks' => $quiz->obj->showblocks ?? 0,
+ 'completionattemptsexhausted' => $quiz->obj->completionattemptsexhausted ?? 0,
+ 'completionpass' => $quiz->obj->completionpass ?? 0,
+ 'completionminattempts' => $quiz->obj->completionminattempts ?? 0,
+ 'allowofflineattempts' => $quiz->obj->allowofflineattempts ?? 0,
+ 'users' => [],
+ 'files' => [],
+ ];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Retrieves the questions for a specific quiz.
+ */
+ private function getQuestionsForQuiz(int $quizId): array
+ {
+ $questions = [];
+ $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? [];
+
+ foreach ($quizResources as $questionId => $questionData) {
+ if (in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids)) {
+ $questions[] = [
+ 'id' => $questionData->source_id,
+ 'questiontext' => $questionData->question,
+ 'qtype' => $this->mapQuestionType($questionData->quiz_type),
+ 'questioncategoryid' => $questionData->question_category ?? 0,
+ 'answers' => $this->getAnswersForQuestion($questionData->source_id),
+ 'maxmark' => $questionData->ponderation ?? 1,
+ ];
+ }
+ }
+
+ return $questions;
+ }
+
+ /**
+ * Maps the quiz type code to a descriptive string.
+ */
+ private function mapQuestionType(string $quizType): string
+ {
+ switch ($quizType) {
+ case UNIQUE_ANSWER: return 'multichoice';
+ case MULTIPLE_ANSWER: return 'multichoice_nosingle';
+ case FILL_IN_BLANKS: return 'match';
+ case FREE_ANSWER: return 'shortanswer';
+ case CALCULATED_ANSWER: return 'calculated';
+ case UPLOAD_ANSWER: return 'fileupload';
+ default: return 'unknown';
+ }
+ }
+
+ /**
+ * Retrieves the answers for a specific question ID.
+ */
+ private function getAnswersForQuestion(int $questionId): array
+ {
+ $answers = [];
+ $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? [];
+
+ foreach ($quizResources as $questionData) {
+ if ($questionData->source_id == $questionId) {
+ foreach ($questionData->answers as $answer) {
+ $answers[] = [
+ 'text' => $answer['answer'],
+ 'fraction' => $answer['correct'] == '1' ? 100 : 0,
+ 'feedback' => $answer['comment'],
+ ];
+ }
+ }
+ }
+ return $answers;
+ }
+
+ /**
+ * Retrieves feedbacks for a specific quiz.
+ */
+ private function getFeedbacksForQuiz(int $quizId): array
+ {
+ $feedbacks = [];
+ $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? [];
+
+ foreach ($quizResources as $quiz) {
+ if ($quiz->obj->iid == $quizId) {
+ $feedbacks[] = [
+ 'feedbacktext' => $quiz->obj->description ?? '',
+ 'mingrade' => 0.00000,
+ 'maxgrade' => $quiz->obj->grade ?? 10.00000,
+ ];
+ }
+ }
+
+ return $feedbacks;
+ }
+
+ /**
+ * Creates the quiz.xml file.
+ */
+ private function createQuizXml(array $quizData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($quizData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($quizData['intro']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['timeopen'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['timeclose'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['timelimit'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['overduehandling'] ?? 'autosubmit') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['graceperiod'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($quizData['preferredbehaviour']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['canredoquestions'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['attempts_number'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['attemptonlast'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $quizData['grademethod'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $quizData['decimalpoints'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['questiondecimalpoints'] ?? -1) . '' . PHP_EOL;
+
+ // Review options
+ $xmlContent .= ' ' . ($quizData['reviewattempt'] ?? 69888) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['reviewcorrectness'] ?? 4352) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['reviewmarks'] ?? 4352) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['reviewspecificfeedback'] ?? 4352) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['reviewgeneralfeedback'] ?? 4352) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['reviewrightanswer'] ?? 4352) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['reviewoverallfeedback'] ?? 4352) . '' . PHP_EOL;
+
+ // Navigation and presentation settings
+ $xmlContent .= ' ' . $quizData['questionsperpage'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($quizData['navmethod']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $quizData['shuffleanswers'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $quizData['sumgrades'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $quizData['grade'] . '' . PHP_EOL;
+
+ // Timing and security
+ $xmlContent .= ' ' . ($quizData['timecreated'] ?? time()) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['timemodified'] ?? time()) . '' . PHP_EOL;
+ $xmlContent .= ' ' . (isset($quizData['password']) ? htmlspecialchars($quizData['password']) : '') . '' . PHP_EOL;
+ $xmlContent .= ' ' . (isset($quizData['subnet']) ? htmlspecialchars($quizData['subnet']) : '') . '' . PHP_EOL;
+ $xmlContent .= ' ' . (isset($quizData['browsersecurity']) ? htmlspecialchars($quizData['browsersecurity']) : '-') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['delay1'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['delay2'] ?? 0) . '' . PHP_EOL;
+
+ // Additional options
+ $xmlContent .= ' ' . ($quizData['showuserpicture'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['showblocks'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['completionattemptsexhausted'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['completionpass'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['completionminattempts'] ?? 0) . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($quizData['allowofflineattempts'] ?? 0) . '' . PHP_EOL;
+
+ // Subplugin, if applicable
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Add question instances
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($quizData['questions'] as $question) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $question['id'] . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . $question['id'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $question['questioncategoryid'] . '' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' ' . $question['maxmark'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Quiz sections
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Add feedbacks
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($quizData['feedbacks'] as $feedback) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($feedback['feedbacktext']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . $feedback['mingrade'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $feedback['maxgrade'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+
+ // Complete with placeholders for attempts and grades
+ $xmlContent .= ' ' . PHP_EOL . ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL . ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL . ' ' . PHP_EOL;
+
+ // Close the activity tag
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '' . PHP_EOL;
+
+ // Save the XML file
+ $xmlFile = $destinationDir . '/quiz.xml';
+ if (file_put_contents($xmlFile, $xmlContent) === false) {
+ throw new Exception(get_lang('ErrorCreatingQuizXml'));
+ }
+ }
+
+ /**
+ * Exports a question in XML format.
+ */
+ public function exportQuestion(array $question): string
+ {
+ $xmlContent = '
' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($question['questiontext'] ?? 'No question text') . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($question['questiontext'] ?? 'No question text') . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . ($question['maxmark'] ?? '0') . '' . PHP_EOL;
+ $xmlContent .= ' 0.3333333' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars(str_replace('_nosingle', '', $question['qtype']) ?? 'unknown') . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' moodle+' . time() . '+QUESTIONSTAMP' . PHP_EOL;
+ $xmlContent .= ' moodle+' . time() . '+VERSIONSTAMP' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . time() . '' . PHP_EOL;
+ $xmlContent .= ' ' . time() . '' . PHP_EOL;
+ $xmlContent .= ' 2' . PHP_EOL;
+ $xmlContent .= ' 2' . PHP_EOL;
+
+ // Add question type-specific content
+ switch ($question['qtype']) {
+ case 'multichoice':
+ $xmlContent .= $this->exportMultichoiceQuestion($question);
+ break;
+ case 'multichoice_nosingle':
+ $xmlContent .= $this->exportMultichoiceNosingleQuestion($question);
+ break;
+ case 'truefalse':
+ $xmlContent .= $this->exportTrueFalseQuestion($question);
+ break;
+ case 'shortanswer':
+ $xmlContent .= $this->exportShortAnswerQuestion($question);
+ break;
+ case 'match':
+ $xmlContent .= $this->exportMatchQuestion($question);
+ break;
+ }
+
+ $xmlContent .= ' ' . PHP_EOL;
+
+ return $xmlContent;
+ }
+
+ /**
+ * Exports a multiple-choice question in XML format.
+ */
+ private function exportMultichoiceQuestion(array $question): string
+ {
+ $xmlContent = '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($question['answers'] as $answer) {
+ $xmlContent .= $this->exportAnswer($answer);
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' Your answer is correct.' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' Your answer is partially correct.' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' Your answer is incorrect.' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' abc' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ return $xmlContent;
+ }
+
+ /**
+ * Exports a multiple-choice question with single=0 in XML format.
+ */
+ private function exportMultichoiceNosingleQuestion(array $question): string
+ {
+ // Similar structure to exportMultichoiceQuestion, but with single=0
+ $xmlContent = str_replace('
1', '
0', $this->exportMultichoiceQuestion($question));
+ return $xmlContent;
+ }
+
+ /**
+ * Exports a true/false question in XML format.
+ */
+ private function exportTrueFalseQuestion(array $question): string
+ {
+ $xmlContent = '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($question['answers'] as $answer) {
+ $xmlContent .= $this->exportAnswer($answer);
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . ($question['answers'][0]['id'] ?? '0') . '' . PHP_EOL;
+ $xmlContent .= ' ' . ($question['answers'][1]['id'] ?? '0') . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ return $xmlContent;
+ }
+
+ /**
+ * Exports a short answer question in XML format.
+ */
+ private function exportShortAnswerQuestion(array $question): string
+ {
+ $xmlContent = '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ foreach ($question['answers'] as $answer) {
+ $xmlContent .= $this->exportAnswer($answer);
+ }
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ return $xmlContent;
+ }
+
+ /**
+ * Exports a matching question in XML format.
+ */
+ private function exportMatchQuestion(array $question): string
+ {
+ $xmlContent = '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($question['correctfeedback'] ?? '') . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($question['partiallycorrectfeedback'] ?? '') . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($question['incorrectfeedback'] ?? '') . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $res = FillBlanks::getAnswerInfo($question['answers'][0]['text']);
+ $words = $res['words'];
+ $common_words = $res['common_words'];
+
+ for ($i = 0; $i < count($common_words); $i++) {
+ $answer = htmlspecialchars(trim(strip_tags($common_words[$i])));
+ if (!empty(trim($answer))) {
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . $answer . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars(explode('|', $words[$i])[0]) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ }
+ }
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ return $xmlContent;
+ }
+
+ /**
+ * Exports an answer in XML format.
+ */
+ private function exportAnswer(array $answer): string
+ {
+ return '
' . PHP_EOL .
+ ' ' . htmlspecialchars($answer['text'] ?? 'No answer text') . '' . PHP_EOL .
+ ' 1' . PHP_EOL .
+ ' ' . ($answer['fraction'] ?? '0') . '' . PHP_EOL .
+ ' ' . htmlspecialchars($answer['feedback'] ?? '') . '' . PHP_EOL .
+ ' 1' . PHP_EOL .
+ ' ' . PHP_EOL;
+ }
+}
+
diff --git a/main/inc/lib/moodleexport/ResourceExport.php b/main/inc/lib/moodleexport/ResourceExport.php
new file mode 100644
index 00000000000..e1e810183e4
--- /dev/null
+++ b/main/inc/lib/moodleexport/ResourceExport.php
@@ -0,0 +1,111 @@
+prepareActivityDirectory($exportDir, 'resource', $moduleId);
+
+ // Retrieve resource data
+ $resourceData = $this->getData($activityId, $sectionId);
+
+ // Generate XML files
+ $this->createResourceXml($resourceData, $resourceDir);
+ $this->createModuleXml($resourceData, $resourceDir);
+ $this->createGradesXml($resourceData, $resourceDir);
+ $this->createFiltersXml($resourceData, $resourceDir);
+ $this->createGradeHistoryXml($resourceData, $resourceDir);
+ $this->createInforefXml($resourceData, $resourceDir);
+ $this->createRolesXml($resourceData, $resourceDir);
+ $this->createCommentsXml($resourceData, $resourceDir);
+ $this->createCalendarXml($resourceData, $resourceDir);
+ }
+
+ /**
+ * Create the XML file for the resource.
+ */
+ private function createResourceXml(array $resourceData, string $resourceDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($resourceData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($resourceData['intro']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' $@NULL@$' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' a:1:{s:10:"printintro";i:1;}' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . $resourceData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '' . PHP_EOL;
+
+ $this->createXmlFile('resource', $xmlContent, $resourceDir);
+ }
+
+ /**
+ * Creates the inforef.xml file, referencing users and files associated with the activity.
+ *
+ * @param array $references Contains 'users' and 'files' arrays to reference in the XML.
+ * @param string $directory The directory where the XML file will be saved.
+ */
+ protected function createInforefXml(array $references, string $directory): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($references['id']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+
+ $xmlContent .= '' . PHP_EOL;
+
+ // Save the XML content to the directory
+ $this->createXmlFile('inforef', $xmlContent, $directory);
+ }
+
+ /**
+ * Get resource data dynamically from the course.
+ */
+ public function getData(int $resourceId, int $sectionId): array
+ {
+ $resource = $this->course->resources[RESOURCE_DOCUMENT][$resourceId];
+
+ return [
+ 'id' => $resourceId,
+ 'moduleid' => $resource->source_id,
+ 'modulename' => 'resource',
+ 'contextid' => $resource->source_id,
+ 'name' => $resource->title,
+ 'intro' => $resource->comment ?? '',
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 1,
+ 'timemodified' => time(),
+ 'users' => [],
+ 'files' => [],
+ ];
+ }
+}
diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php
new file mode 100644
index 00000000000..77b8624c993
--- /dev/null
+++ b/main/inc/lib/moodleexport/SectionExport.php
@@ -0,0 +1,322 @@
+course = $course;
+ }
+
+ /**
+ * 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' => get_lang('General'),
+ 'summary' => 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);
+ }
+
+ /**
+ * 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.");
+ }
+ }
+ }
+
+ /**
+ * Get all general items not linked to any lesson (learnpath).
+ */
+ public function getGeneralItems(): array
+ {
+ $generalItems = [];
+
+ // List of resource types and their corresponding ID keys
+ $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',
+ ];
+
+ 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
+ ? ($resource->params['title'] ?? '')
+ : ($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
+ {
+ $generalLearnpath = (object) [
+ 'items' => $this->getGeneralItems(),
+ 'source_id' => 0
+ ];
+
+ return $this->getActivitiesForSection($generalLearnpath, true);
+ }
+
+ /**
+ * Check if an item is associated with any learnpath.
+ */
+ private function isItemInLearnpath(object $item, string $type): bool
+ {
+ if (!empty($this->course->resources[RESOURCE_LEARNPATH])) {
+ foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
+ if (!empty($learnpath->items)) {
+ foreach ($learnpath->items as $learnpathItem) {
+ if ($learnpathItem['item_type'] === $type && $learnpathItem['path'] == $item->source_id) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the learnpath object by its ID.
+ */
+ public function getLearnpathById(int $sectionId): ?object
+ {
+ foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
+ if ($learnpath->source_id == $sectionId) {
+ return $learnpath;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get section data for a learnpath.
+ */
+ public function getSectionData(object $learnpath): array
+ {
+ return [
+ 'id' => $learnpath->source_id,
+ 'number' => $learnpath->display_order,
+ 'name' => $learnpath->name,
+ 'summary' => $learnpath->description,
+ 'sequence' => $learnpath->source_id,
+ 'visible' => $learnpath->visibility,
+ 'timemodified' => strtotime($learnpath->modified_on),
+ 'activities' => $this->getActivitiesForSection($learnpath)
+ ];
+ }
+
+ /**
+ * Get the activities for a specific section.
+ */
+ public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array
+ {
+ $activities = [];
+ $sectionId = $isGeneral ? 0 : $learnpath->source_id;
+
+ foreach ($learnpath->items as $item) {
+ $this->addActivityToList($item, $sectionId, $activities);
+ }
+
+ return $activities;
+ }
+
+ /**
+ * Add an activity to the activities list.
+ */
+ private function addActivityToList(array $item, int $sectionId, array &$activities): void
+ {
+ $activityData = null;
+ $activityClassMap = [
+ '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,
+ ];
+
+ $itemType = $item['item_type'] === 'link' ? 'url' : ($item['item_type'] === 'work' ? 'assign' : ($item['item_type'] === 'survey' ? 'feedback' : $item['item_type']));
+
+ switch ($itemType) {
+ case 'quiz':
+ case 'glossary':
+ case 'assign':
+ case 'url':
+ case 'forum':
+ case 'feedback':
+ $activityId = $itemType === 'glossary' ? 1 : (int) $item['path'];
+ $exportClass = $activityClassMap[$itemType];
+ $exportInstance = new $exportClass($this->course);
+ $activityData = $exportInstance->getData($activityId, $sectionId);
+ break;
+
+ case 'document':
+ $documentId = (int) $item['path'];
+ $document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code);
+
+ // Determine the type of document and get the corresponding export class
+ $documentType = $this->getDocumentType($document['filetype'], $document['path']);
+ if ($documentType) {
+ $activityClass = $activityClassMap[$documentType];
+ $exportInstance = new $activityClass($this->course);
+ $activityData = $exportInstance->getData($item['path'], $sectionId);
+ }
+ break;
+ }
+
+ // Add the activity to the list if the data exists
+ if ($activityData) {
+ $activities[] = [
+ 'id' => $activityData['id'],
+ 'moduleid' => $activityData['moduleid'],
+ 'type' => $item['item_type'],
+ 'modulename' => $activityData['modulename'],
+ 'name' => $activityData['name'],
+ ];
+ }
+ }
+
+ /**
+ * Determine the document type based on filetype and path.
+ */
+ private function getDocumentType(string $filetype, string $path): ?string
+ {
+ if ('html' === pathinfo($path, PATHINFO_EXTENSION)) {
+ return 'page';
+ } elseif ('file' === $filetype) {
+ return 'resource';
+ } elseif ('folder' === $filetype) {
+ return 'folder';
+ }
+
+ return null;
+ }
+
+ /**
+ * Create the section.xml file.
+ */
+ private function createSectionXml(array $sectionData, string $destinationDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . $sectionData['number'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($sectionData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($sectionData['summary']) . '' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . implode(',', array_column($sectionData['activities'], 'moduleid')) . '' . PHP_EOL;
+ $xmlContent .= ' ' . $sectionData['visible'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . $sectionData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= '' . PHP_EOL;
+
+ $xmlFile = $destinationDir . '/section.xml';
+ file_put_contents($xmlFile, $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) {
+ $xmlContent .= ' ' . htmlspecialchars($activity['name']) . '' . PHP_EOL;
+ }
+
+ $xmlContent .= '' . PHP_EOL;
+
+ $xmlFile = $destinationDir . '/inforef.xml';
+ file_put_contents($xmlFile, $xmlContent);
+ }
+}
diff --git a/main/inc/lib/moodleexport/UrlExport.php b/main/inc/lib/moodleexport/UrlExport.php
new file mode 100644
index 00000000000..96c728c74f9
--- /dev/null
+++ b/main/inc/lib/moodleexport/UrlExport.php
@@ -0,0 +1,89 @@
+prepareActivityDirectory($exportDir, 'url', $moduleId);
+
+ // Retrieve URL data
+ $urlData = $this->getData($activityId, $sectionId);
+
+ // Generate XML file for the URL
+ $this->createUrlXml($urlData, $urlDir);
+ $this->createModuleXml($urlData, $urlDir);
+ $this->createGradesXml($urlData, $urlDir);
+ $this->createGradeHistoryXml($urlData, $urlDir);
+ $this->createInforefXml($urlData, $urlDir);
+ $this->createRolesXml($urlData, $urlDir);
+ $this->createCommentsXml($urlData, $urlDir);
+ $this->createCalendarXml($urlData, $urlDir);
+ $this->createFiltersXml($urlData, $urlDir);
+ }
+
+ /**
+ * Create the XML file for the URL.
+ */
+ private function createUrlXml(array $urlData, string $urlDir): void
+ {
+ $xmlContent = '' . PHP_EOL;
+ $xmlContent .= '
' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($urlData['name']) . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= ' 1' . PHP_EOL;
+ $xmlContent .= ' ' . htmlspecialchars($urlData['externalurl']) . '' . PHP_EOL;
+ $xmlContent .= ' 0' . PHP_EOL;
+ $xmlContent .= ' a:1:{s:10:"printintro";i:1;}' . PHP_EOL;
+ $xmlContent .= ' a:0:{}' . PHP_EOL;
+ $xmlContent .= ' ' . $urlData['timemodified'] . '' . PHP_EOL;
+ $xmlContent .= ' ' . PHP_EOL;
+ $xmlContent .= '';
+
+ $this->createXmlFile('url', $xmlContent, $urlDir);
+ }
+
+ /**
+ * Get all URL data for the course.
+ */
+ public function getData(int $activityId, int $sectionId): ?array
+ {
+ // Extract the URL information from the course data
+ $url = $this->course->resources['link'][$activityId];
+
+ // Return the URL data formatted for export
+ return [
+ 'id' => $activityId,
+ 'moduleid' => $activityId,
+ 'modulename' => 'url',
+ 'contextid' => $this->course->info['real_id'],
+ 'name' => $url->title,
+ 'description' => $url->description,
+ 'externalurl' => $url->url,
+ 'timecreated' => time(),
+ 'timemodified' => time(),
+ 'sectionid' => $sectionId,
+ 'sectionnumber' => 0,
+ 'users' => [],
+ 'files' => [],
+ ];
+ }
+}
diff --git a/src/Chamilo/CourseBundle/Component/CourseCopy/CourseSelectForm.php b/src/Chamilo/CourseBundle/Component/CourseCopy/CourseSelectForm.php
index bc212949237..9770aaa1ac8 100644
--- a/src/Chamilo/CourseBundle/Component/CourseCopy/CourseSelectForm.php
+++ b/src/Chamilo/CourseBundle/Component/CourseCopy/CourseSelectForm.php
@@ -203,10 +203,10 @@ function check_topic(obj) {
echo Display::return_message(get_lang('DontForgetToSelectTheMediaFilesIfYourResourceNeedIt'));
$resource_titles = self::getResourceTitleList();
- $element_count = self::parseResources($resource_titles, $course->resources, true, true);
+ $element_count = self::parseResources($resource_titles, $course->resources, $forum_categories, $forums, $forum_topics,true, true);
// Fixes forum order
- if (!empty($forum_categories)) {
+ if (!empty($element_count)) {
$type = RESOURCE_FORUMCATEGORY;
echo '
';
echo '';
@@ -335,6 +335,9 @@ class="save btn btn-primary"
public static function parseResources(
$resource_titles,
$resourceList,
+ &$forum_categories,
+ &$forums,
+ &$forum_topics,
$showHeader = true,
$showItems = true
) {
diff --git a/src/Chamilo/CourseBundle/Component/CourseCopy/Resources/SurveyQuestion.php b/src/Chamilo/CourseBundle/Component/CourseCopy/Resources/SurveyQuestion.php
index acbe003365b..f689e37e9a7 100644
--- a/src/Chamilo/CourseBundle/Component/CourseCopy/Resources/SurveyQuestion.php
+++ b/src/Chamilo/CourseBundle/Component/CourseCopy/Resources/SurveyQuestion.php
@@ -52,6 +52,8 @@ class SurveyQuestion extends Resource
*/
public $is_required;
+ public $id;
+
/**
* Create a new SurveyQuestion.
*
@@ -79,6 +81,7 @@ public function __construct(
$is_required = false
) {
parent::__construct($id, RESOURCE_SURVEYQUESTION);
+ $this->id = $id;
$this->survey_id = $survey_id;
$this->survey_question = $survey_question;
$this->survey_question_comment = $survey_question_comment;