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 @@
+
  • + + +
    + +
  • diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php new file mode 100644 index 00000000000..b061bf9f219 --- /dev/null +++ b/main/coursecopy/export_moodle.php @@ -0,0 +1,179 @@ + api_get_path(WEB_CODE_PATH).'course_info/maintenance.php', + 'name' => get_lang('Maintenance'), +]; + +// Displaying the header +$nameTools = get_lang('ExportToMoodle'); +Display::display_header($nameTools); + +// Display the tool title +echo Display::page_header($nameTools); +$action = isset($_POST['action']) ? $_POST['action'] : ''; +$exportOption = isset($_POST['export_option']) ? $_POST['export_option'] : ''; + +// Handle course selection form submission +if ($action === 'course_select_form' && Security::check_token('post')) { + // Handle the selected resources and continue with export + $selectedResources = $_POST['resource'] ?? null; + + if (!empty($selectedResources)) { + // Rebuild the course object based on selected resources + $cb = new CourseBuilder('partial'); + $course = $cb->build(0, null, false, array_keys($selectedResources), $selectedResources); + + // Get admin details + $adminId = (int) $_POST['admin_id']; + $adminUsername = filter_var($_POST['admin_username'], FILTER_SANITIZE_STRING); + if (!preg_match('/^[a-zA-Z0-9_]+$/', $adminUsername)) { + echo Display::return_message(get_lang('PleaseEnterValidLogin'), 'error'); + exit(); + } + + $adminEmail = filter_var($_POST['admin_email'], FILTER_SANITIZE_EMAIL); + if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) { + echo Display::return_message(get_lang('PleaseEnterValidEmail'), 'error'); + exit(); + } + + $exporter = new MoodleExport($course); + $exporter->setAdminUserData($adminId, $adminUsername, $adminEmail); + + // Perform export + $courseId = api_get_course_id(); + $exportDir = 'moodle_export_' . $courseId; + try { + $moodleVersion = isset($_POST['moodle_version']) ? (int) $_POST['moodle_version'] : 3; + $mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion); + + echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm'); + echo '
    '; + echo Display::url( + get_lang('Download'), + api_get_path(WEB_CODE_PATH).'course_info/download.php?archive_path=1&archive='.basename($mbzFile).'&'.api_get_cidreq(), + ['class' => 'btn btn-primary btn-large'] + ); + } catch (Exception $e) { + echo Display::return_message(get_lang('ErrorCreatingExport').': '.$e->getMessage(), 'error'); + } + exit(); + } else { + echo Display::return_message(get_lang('NoResourcesSelected'), 'warning'); + } +} else { + $form = new FormValidator( + 'create_export_form', + 'post', + api_get_self().'?'.api_get_cidreq() + ); + $form->addElement('radio', 'export_option', '', get_lang('CreateFullExport'), 'full_export'); + $form->addElement('radio', 'export_option', '', get_lang('LetMeSelectItems'), 'select_items'); + $form->addElement('select', 'moodle_version', get_lang('MoodleVersion'), [ + '3' => 'Moodle 3.x', + '4' => 'Moodle 4.x', + ]); + + $form->addElement('text', 'admin_id', get_lang('AdminID'), ['maxlength' => 10, 'size' => 10]); + $form->addElement('text', 'admin_username', get_lang('AdminUsername'), ['maxlength' => 100, 'size' => 50]); + $form->addElement('text', 'admin_email', get_lang('AdminEmail'), ['maxlength' => 100, 'size' => 50]); + + // Add validation rules + $form->addRule('admin_id', get_lang('ThisFieldIsRequired'), 'required'); + $form->addRule('admin_username', get_lang('ThisFieldIsRequired'), 'required'); + $form->addRule('admin_email', get_lang('ThisFieldIsRequired'), 'required'); + $form->addRule('admin_email', get_lang('EnterValidEmail'), 'email'); + + $values['export_option'] = 'select_items'; + $form->setDefaults($values); + + // Add buttons + $form->addButtonSave(get_lang('CreateExport')); + $form->addProgress(); + + if ($form->validate()) { + $values = $form->exportValues(); + $adminId = (int) $values['admin_id']; + $adminUsername = $values['admin_username']; + $adminEmail = $values['admin_email']; + + if ($values['export_option'] === 'full_export') { + $cb = new CourseBuilder('complete'); + $course = $cb->build(); + + $exporter = new MoodleExport($course); + $exporter->setAdminUserData($adminId, $adminUsername, $adminEmail); + + $courseId = api_get_course_id(); // Get course ID + $exportDir = 'moodle_export_' . $courseId; + + try { + $moodleVersion = isset($values['moodle_version']) ? $values['moodle_version'] : '3'; + $mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion); + echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm'); + echo '
    '; + echo Display::url( + get_lang('Download'), + api_get_path(WEB_CODE_PATH).'course_info/download.php?archive_path=1&archive='.basename($mbzFile).'&'.api_get_cidreq(), + ['class' => 'btn btn-primary btn-large'] + ); + } catch (Exception $e) { + echo Display::return_message(get_lang('ErrorCreatingExport').': '.$e->getMessage(), 'error'); + } + } elseif ($values['export_option'] === 'select_items') { + // Partial export - go to the item selection step + $cb = new CourseBuilder('partial'); + $course = $cb->build(); + if ($course->has_resources()) { + // Add token to Course select form + $hiddenFields['sec_token'] = Security::get_token(); + $hiddenFields['admin_id'] = $adminId; + $hiddenFields['admin_username'] = $adminUsername; + $hiddenFields['admin_email'] = $adminEmail; + + CourseSelectForm::display_form($course, $hiddenFields, false, true); + } else { + echo Display::return_message(get_lang('NoResourcesToExport'), 'warning'); + } + } + } else { + echo '
    '; + 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 .= ' ' . 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;