From 7f4dd09c0494d6fa43a8f7bf5afe287b221cb5f1 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Thu, 5 Sep 2024 23:52:32 -0500 Subject: [PATCH 1/8] Course: Implement MBZ file generation for Moodle 3/4 backups (single-choice quizzes) --- main/course_info/download.php | 4 +- main/course_info/maintenance.php | 6 + main/coursecopy/export_moodle.php | 141 +++++ main/inc/lib/moodleexport/CourseExport.php | 325 ++++++++++ main/inc/lib/moodleexport/FileExport.php | 133 ++++ main/inc/lib/moodleexport/MoodleExport.php | 619 ++++++++++++++++++ main/inc/lib/moodleexport/QuizExport.php | 657 ++++++++++++++++++++ main/inc/lib/moodleexport/SectionExport.php | 172 +++++ 8 files changed, 2055 insertions(+), 2 deletions(-) create mode 100644 main/coursecopy/export_moodle.php create mode 100644 main/inc/lib/moodleexport/CourseExport.php create mode 100644 main/inc/lib/moodleexport/FileExport.php create mode 100644 main/inc/lib/moodleexport/MoodleExport.php create mode 100644 main/inc/lib/moodleexport/QuizExport.php create mode 100644 main/inc/lib/moodleexport/SectionExport.php 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..5753bd54fff --- /dev/null +++ b/main/coursecopy/export_moodle.php @@ -0,0 +1,141 @@ + 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'] : ''; + +if (Security::check_token('post') && + ($action === 'course_select_form' || $exportOption === 'full_export') +) { + // Clear token + Security::clear_token(); + + // Build course object based on the action + if ($action === 'course_select_form') { + $cb = new CourseBuilder('partial'); + $course = $cb->build(0, null, false, array_keys($_POST['resource']), $_POST['resource']); + $course = CourseSelectForm::get_posted_course(null, 0, '', $course); + } else { + $cb = new CourseBuilder('complete'); + $course = $cb->build(); + } + + // Instantiate MoodleExport and pass course data to it + $exporter = new MoodleExport($course); + $courseId = api_get_course_id(); // Get course ID + $exportDir = 'moodle_export_' . $courseId; + $coursePath = api_get_course_path(); // Get course path + + try { + $moodleVersion = isset($_POST['moodle_version']) ? $_POST['moodle_version'] : '3'; + + // Pass the course data to the export function + $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 (Security::check_token('post') && $exportOption === 'select_items') { + // Clear token + Security::clear_token(); + + // Build course object for partial export + $cb = new CourseBuilder('partial'); + $course = $cb->build(); + if ($course->has_resources()) { + // Add token to Course select form + $hiddenFields['sec_token'] = Security::get_token(); + CourseSelectForm::display_form($course, $hiddenFields, false, true); + } else { + echo Display::return_message(get_lang('NoResourcesToExport'), 'warning'); + } +} else { + $form = new FormValidator( + 'create_export_form', + 'post', + api_get_self().'?'.api_get_cidreq() + ); + $form->addElement('header', get_lang('SelectOptionForExport')); + $form->addElement('radio', 'export_option', '', get_lang('CreateFullExport'), 'full_export'); + $form->addElement('radio', 'export_option', '', get_lang('LetMeSelectItems'), 'select_items'); + + // Add Moodle version selection + $form->addElement('header', get_lang('SelectMoodleVersion')); + $form->addElement('select', 'moodle_version', get_lang('MoodleVersion'), [ + '3' => 'Moodle 3.x', + '4' => 'Moodle 4.x', + ]); + + $form->addButtonSave(get_lang('CreateExport')); + $form->addProgress(); + // When progress bar appears we have to hide the title "Please select an export-option". + $form->updateAttributes( + [ + 'onsubmit' => str_replace( + 'javascript: ', + 'javascript: page_title = getElementById(\'page_title\'); if (page_title) { setTimeout(\'page_title.style.display = \\\'none\\\';\', 2000); } ', + $form->getAttribute('onsubmit') + ), + ] + ); + $values['export_option'] = 'full_export'; + $form->setDefaults($values); + + // Add Security token + $token = Security::get_token(); + $form->addElement('hidden', 'sec_token'); + $form->setConstants(['sec_token' => $token]); + echo '
    '; + echo '
    '; + echo '
    '; + $form->display(); + echo '
    '; + echo '
    '; + echo '
    '; +} + +Display::display_footer(); diff --git a/main/inc/lib/moodleexport/CourseExport.php b/main/inc/lib/moodleexport/CourseExport.php new file mode 100644 index 00000000000..51e73221d43 --- /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. + * + * @param string $exportDir The main export directory (where `course/` will be created). + */ + public function exportCourse($exportDir) + { + // Create the course directory if it doesn't exist + $courseDir = $exportDir . '/course'; + if (!is_dir($courseDir)) { + mkdir($courseDir, api_get_permissions_for_new_directories(), true); + } + + $this->createCourseXml($this->courseInfo, $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. + * + * @param array $courseInfo The course data passed from MoodleExport. + * @param string $destinationDir The directory where the XML will be saved. + */ + private function createCourseXml($courseInfo, $destinationDir) + { + $courseId = $courseInfo['id'] ?? 0; + $contextId = $courseInfo['id'] ?? 1; + $shortname = $courseInfo['code'] ?? 'Unknown Course'; + $fullname = $courseInfo['title'] ?? 'Unknown Fullname'; + $showgrades = $courseInfo['showgrades'] ?? 0; + $startdate = $courseInfo['startdate'] ?? time(); + $enddate = $courseInfo['enddate'] ?? time() + (60 * 60 * 24 * 365); + $visible = $courseInfo['visible'] ?? 1; + $enablecompletion = $courseInfo['enablecompletion'] ?? 0; + + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($shortname) . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($fullname) . '' . PHP_EOL; + $xmlContent .= ' topics' . PHP_EOL; + $xmlContent .= ' ' . $showgrades . '' . PHP_EOL; + $xmlContent .= ' ' . $startdate . '' . PHP_EOL; + $xmlContent .= ' ' . $enddate . '' . PHP_EOL; + $xmlContent .= ' ' . $visible . '' . PHP_EOL; + $xmlContent .= ' ' . $enablecompletion . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' Miscellaneous' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ''; + + file_put_contents($destinationDir . '/course.xml', $xmlContent); + } + + /** + * Create enrolments.xml based on the course data from MoodleExport. + * + * @param array $enrolmentsData The enrolments data passed from MoodleExport. + * @param string $destinationDir The directory where the XML will be saved. + */ + private function createEnrolmentsXml($enrolmentsData, $destinationDir) + { + $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 and question categories. + * + * @param string $destinationDir The directory where the XML file will be saved. + * + * @return void + */ + private function createInforefXml($destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $questionCategories = []; + foreach ($this->activities as $activity) { + if ($activity['modulename'] === 'quiz') { + $quizExport = new QuizExport($this->course); + $quizData = $quizExport->getQuizData($activity['id'], $activity['sectionid']); + foreach ($quizData['questions'] as $question) { + $categoryId = $question['questioncategoryid']; + if (!in_array($categoryId, $questionCategories, true)) { + $questionCategories[] = $categoryId; + } + } + } + } + + $xmlContent .= ' ' . PHP_EOL; + foreach ($questionCategories as $categoryId) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $categoryId . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + file_put_contents($destinationDir . '/inforef.xml', $xmlContent); + } + + /** + * Creates the roles.xml file. + * + * @param array $rolesData Role data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createRolesXml($rolesData, $destinationDir) + { + $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. + * + * @param array $calendarData Calendar event data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCalendarXml($calendarData, $destinationDir) + { + $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. + * + * @param array $commentsData Comment data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCommentsXml($commentsData, $destinationDir) + { + $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. + * + * @param array $competenciesData Competency data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCompetenciesXml($competenciesData, $destinationDir) + { + $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. + * + * @param array $completionData Completion data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCompletionDefaultsXml($completionData, $destinationDir) + { + $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. + * + * @param array $contentBankData Content bank data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createContentBankXml($contentBankData, $destinationDir) + { + $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. + * + * @param array $filtersData Filter data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createFiltersXml($filtersData, $destinationDir) + { + $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/FileExport.php b/main/inc/lib/moodleexport/FileExport.php new file mode 100644 index 00000000000..e97708f9421 --- /dev/null +++ b/main/inc/lib/moodleexport/FileExport.php @@ -0,0 +1,133 @@ +course = $course; + } + + /** + * Export the files and their metadata from files.xml. + * + * @param array $filesData The data from files.xml. + * @param string $exportDir The directory where the files will be stored. + */ + public function exportFiles($filesData, $exportDir) + { + // Directory for storing files + $filesDir = $exportDir . '/files'; + + // Check if the directory exists, if not, create it + if (!is_dir($filesDir)) { + mkdir($filesDir, api_get_permissions_for_new_directories(), true); + } + + // Create a placeholder index.html file to prevent an empty directory + $placeholderFile = $filesDir . '/index.html'; + file_put_contents($placeholderFile, ""); + + // Create files.xml in the root export directory + $this->createFilesXml($filesData, $exportDir); + } + + + /** + * Create the files.xml based on the provided data. + * + * @param array $filesData The data from files.xml. + * @param string $destinationDir The directory where the files.xml will be stored (root export directory). + */ + private function createFilesXml($filesData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + foreach ($filesData['files'] as $file) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['contenthash']) . '' . PHP_EOL; + $xmlContent .= ' ' . $file['contextid'] . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['component']) . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['filearea']) . '' . PHP_EOL; + $xmlContent .= ' ' . $file['itemid'] . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['filepath']) . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['filename']) . '' . PHP_EOL; + $xmlContent .= ' ' . $file['userid'] . '' . PHP_EOL; + $xmlContent .= ' ' . $file['filesize'] . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['mimetype']) . '' . PHP_EOL; + $xmlContent .= ' ' . $file['status'] . '' . PHP_EOL; + $xmlContent .= ' ' . $file['timecreated'] . '' . PHP_EOL; + $xmlContent .= ' ' . $file['timemodified'] . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['source']) . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['author']) . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['license']) . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/files.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Get the file data for testing purposes. Later will be dynamic. + * + * @return array The file data related to activities. + */ + public function getFilesData() + { + return [ + 'files' => [ + [ + 'id' => 1, + 'contenthash' => 'abcd1234efgh5678ijkl', + 'contextid' => 26, + 'component' => 'mod_assign', + 'filearea' => 'submission_files', + 'itemid' => 3, + 'filepath' => '/', + 'filename' => 'assignment_submission_001.pdf', + 'userid' => 5, + 'filesize' => 204800, + 'mimetype' => 'application/pdf', + 'status' => 0, + 'timecreated' => time() - 3600, + 'timemodified' => time(), + 'source' => 'Original Submission', + 'author' => 'Student Name', + 'license' => 'allrightsreserved', + ], + [ + 'id' => 2, + 'contenthash' => 'ijkl1234mnop5678qrst', + 'contextid' => 26, + 'component' => 'mod_quiz', + 'filearea' => 'feedback_files', + 'itemid' => 7, + 'filepath' => '/', + 'filename' => 'quiz_feedback_001.pdf', + 'userid' => 6, + 'filesize' => 102400, + 'mimetype' => 'application/pdf', + 'status' => 0, + 'timecreated' => time() - 7200, + 'timemodified' => time(), + 'source' => 'Quiz Feedback', + 'author' => 'Teacher Name', + 'license' => 'allrightsreserved', + ], + ] + ]; + } +} diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php new file mode 100644 index 00000000000..f06e620d6cd --- /dev/null +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -0,0 +1,619 @@ +course = $course; + } + + /** + * Export a course in Moodle format (.mbz). + * + * @param int $courseId The ID of the course to be exported. + * @param string $exportDir The directory where the export will be created. + * @param string $version The version of Moodle (3 or 4). + * + * @return bool|string Returns the path of the exported file or false in case of error. + * @throws Exception If an error occurs during export. + */ + public function export($courseId, $exportDir, $version) + { + // Temporary directory where the export will be saved + $tempDir = api_get_path(SYS_ARCHIVE_PATH) . $exportDir; + + // Create the export directory if it doesn't exist + if (!is_dir($tempDir)) { + if (!mkdir($tempDir, api_get_permissions_for_new_directories(), true)) { + throw new Exception(get_lang('ErrorCreatingDirectory')); + } + } + + // Get course information + $courseInfo = api_get_course_info($courseId); + if (!$courseInfo) { + throw new Exception(get_lang('CourseNotFound')); + } + + $this->createMoodleBackupXml($tempDir, $version); + + $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 if necessary + $this->cleanupTempDir($tempDir); + + return $exportedFile; + } + + + /** + * Export all root XML files for the course. + * + * This method generates XML files for various aspects of the course including badges, completion, gradebook, + * grade history, groups, outcomes, and questions. It dynamically retrieves activities and quizzes, and exports + * questions associated with each quiz. + * + * @param string $exportDir The directory where XML files will be saved. + * + * @return void + */ + private function exportRootXmlFiles($exportDir) + { + $this->exportBadgesXml($exportDir); + $this->exportCompletionXml($exportDir); + $this->exportGradebookXml($exportDir); + $this->exportGradeHistoryXml($exportDir); + $this->exportGroupsXml($exportDir); + $this->exportOutcomesXml($exportDir); + + $activities = $this->getActivities(); + $questionsData = []; + + foreach ($activities as $activity) { + if ($activity['modulename'] === 'quiz') { + $quizExport = new QuizExport($this->course); + $quizData = $quizExport->getQuizData($activity['id'], $activity['sectionid']); + $questionsData[] = $quizData; + } + } + + $this->exportQuestionsXml($questionsData, $exportDir); + + $this->exportRolesXml($exportDir); + $this->exportScalesXml($exportDir); + $this->exportUsersXml($exportDir); + } + + + /** + * Creates the moodle_backup.xml file with activities, sections, and settings. + * + * @param string $destinationDir The directory where the XML will be saved. + * @param string $version The Moodle version (3 or 4). + * + * @return bool Returns true if the file was created successfully. + */ + private function createMoodleBackupXml($destinationDir, $version) + { + $courseInfo = api_get_course_info($this->course->code); + + $backupId = md5(uniqid(mt_rand(), true)); + $siteHash = md5(uniqid(mt_rand(), true)); + + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + + $wwwRoot = api_get_path(WEB_PATH); + + $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(); // Export backup settings dynamically + foreach ($settings as $setting) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($setting['level']) . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($setting['name']) . '' . PHP_EOL; + $xmlContent .= ' ' . $setting['value'] . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + $xmlContent .= ' ' . PHP_EOL; + + $xmlContent .= '
    ' . PHP_EOL; + $xmlContent .= '
    '; + + $xmlFile = $destinationDir . '/moodle_backup.xml'; + return file_put_contents($xmlFile, $xmlContent) !== false; + } + + /** + * Retrieve all sections from the course. + * + * This method fetches and returns sections from the course resources where the learnpath type is '1'. + * It uses the SectionExport class to get section data based on each learnpath. + * + * @return array An array of section data. + */ + private function getSections() + { + $sectionExport = new SectionExport($this->course); + $sections = []; + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { + if ($learnpath->lp_type == '1') { + $sections[] = $sectionExport->getSectionData($learnpath); + } + } + return $sections; + } + + /** + * Retrieve all activities from the course. + * + * @return array An array of activities. + */ + private function getActivities() + { + $activities = []; + + foreach ($this->course->resources as $resourceType => $resources) { + foreach ($resources as $resource) { + if ($resource->obj->iid > 0) { + if ($resourceType === RESOURCE_QUIZ) { + $quizExport = new QuizExport($this->course); + $activities[] = [ + 'id' => $resource->obj->iid, + 'sectionid' => $quizExport->getSectionIdForQuiz($resource->obj->iid), + 'modulename' => 'quiz', + 'moduleid' => $resource->obj->iid, + 'title' => $resource->obj->title, + ]; + } + + // Add more cases for other types of resources as needed + } + } + } + + return $activities; + } + + /** + * Export badges data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportBadgesXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate badges + $xmlContent .= ''; + $xmlFile = $exportDir . '/badges.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export completion data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportCompletionXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate completion data + $xmlContent .= ''; + $xmlFile = $exportDir . '/completion.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export gradebook data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportGradebookXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate gradebook data + $xmlContent .= ''; + $xmlFile = $exportDir . '/gradebook.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export grade history data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportGradeHistoryXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate grade history data + $xmlContent .= ''; + $xmlFile = $exportDir . '/grade_history.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export groups data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportGroupsXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate groups data + $xmlContent .= ''; + $xmlFile = $exportDir . '/groups.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export outcomes data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportOutcomesXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate outcomes data + $xmlContent .= ''; + $xmlFile = $exportDir . '/outcomes.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export the questions to questions.xml. + * + * @param array $questionsData The data of the questions (from getQuizData). + * @param string $exportDir The directory where the XML will be saved. + */ + private function exportQuestionsXml($questionsData, $exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + // Iterate through each quiz to extract questions + foreach ($questionsData as $quiz) { + // Assuming each entry in $questionsData represents a quiz + $categoryId = $quiz['questions'][0]['questioncategoryid'] ?? 'default_category_id'; // Use a default value if category is not set + + $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 .= ' my.moodle3.com+' . time() . '+CATEGORYSTAMP' . PHP_EOL; + $xmlContent .= ' 0' . PHP_EOL; + $xmlContent .= ' 999' . PHP_EOL; + $xmlContent .= ' $@NULL@$' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + + // Extract each question from the quiz + foreach ($quiz['questions'] as $question) { + $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($question['qtype'] ?? 'unknown') . '' . PHP_EOL; + $xmlContent .= ' 1' . PHP_EOL; + $xmlContent .= ' my.moodle3.com+' . time() . '+QUESTIONSTAMP' . PHP_EOL; + $xmlContent .= ' my.moodle3.com+' . time() . '+VERSIONSTAMP' . PHP_EOL; + $xmlContent .= ' 0' . PHP_EOL; + $xmlContent .= ' ' . time() . '' . PHP_EOL; + $xmlContent .= ' ' . time() . '' . PHP_EOL; + $xmlContent .= ' 2' . PHP_EOL; + $xmlContent .= ' 2' . PHP_EOL; + + // Check if the question type is multichoice and add answers + if ($question['qtype'] === 'multichoice') { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + foreach ($question['answers'] as $answer) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($answer['text'] ?? 'No answer text') . '' . PHP_EOL; + $xmlContent .= ' 1' . PHP_EOL; + $xmlContent .= ' ' . ($answer['fraction'] ?? '0') . '' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($answer['feedback'] ?? '') . '' . PHP_EOL; + $xmlContent .= ' 1' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + $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 .= ' 0' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + + $xmlContent .= ' ' . PHP_EOL; + } + + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + + $xmlContent .= ''; + + $xmlFile = $exportDir . '/questions.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export roles data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportRolesXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate roles data + $xmlContent .= ''; + $xmlFile = $exportDir . '/roles.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export scales data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportScalesXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate scales data + $xmlContent .= ''; + $xmlFile = $exportDir . '/scales.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Export users data to XML file. + * + * @param string $exportDir Directory to save the XML file. + */ + private function exportUsersXml($exportDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + // Populate users data + $xmlContent .= ''; + $xmlFile = $exportDir . '/users.xml'; + file_put_contents($xmlFile, $xmlContent); + } + + /** + * Exports the course sections. + * + * @param int $courseId The ID of the course. + * @param string $exportDir The directory where the sections will be saved. + * + * @return void + */ + private function exportSections($exportDir) + { + $sections = $this->getSections(); + foreach ($sections as $section) { + $sectionExport = new SectionExport($this->course); + $sectionExport->exportSection($section['id'], $exportDir); + } + } + + /** + * Export backup settings dynamically for the course. + * + * @return array + */ + private function exportBackupSettings() + { + // this should be pulled from a configuration + return [ + ['level' => 'root', 'name' => 'users', 'value' => '1'], + ['level' => 'root', 'name' => 'anonymize', 'value' => '0'], + ['level' => 'root', 'name' => 'activities', 'value' => '1'], + // Add more settings as needed + ]; + } + + /** + * Compresses the exported course into an .mbz (ZIP) file. + * + * @param string $sourceDir Directory to compress. + * + * @return string The path of the exported .mbz file. + * @throws Exception If an error occurs while creating the ZIP. + */ + private function createMbzFile($sourceDir) + { + $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 $name => $file) { + // Skip directories + if (!$file->isDir()) { + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen($sourceDir) + 1); + + // Add the file to the ZIP + if (!$zip->addFile($filePath, $relativePath)) { + throw new Exception(get_lang('ErrorAddingFileToZip') . ": $relativePath"); + } + } + } + + // Close the ZIP file + if (!$zip->close()) { + throw new Exception(get_lang('ErrorClosingZip')); + } + + return $zipFile; + } + + /** + * Cleans up the temporary directory used for export. + * + * @param string $dir Directory to delete. + * + * @return void + */ + private function cleanupTempDir($dir) + { + $this->recursiveDelete($dir); + } + + /** + * Recursively deletes a directory and its contents. + * + * @param string $dir Directory to delete. + * + * @return void + */ + private function recursiveDelete($dir) + { + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = "$dir/$file"; + (is_dir($path)) ? $this->recursiveDelete($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php new file mode 100644 index 00000000000..289ec72745c --- /dev/null +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -0,0 +1,657 @@ +course = $course; + } + + /** + * Export a quiz to the specified directory. + * + * @param int $quizId The ID of the quiz. + * @param string $exportDir The directory where the quiz will be exported. + * @param int $moduleId The ID of the module. + * @param int $sectionId The ID of the section. + */ + public function exportQuiz($quizId, $exportDir, $moduleId, $sectionId) + { + // Directory where the quiz export will be saved (activities/quiz_XXX) + $quizDir = $exportDir . "/activities/quiz_{$moduleId}"; + + if (!is_dir($quizDir)) { + mkdir($quizDir, api_get_permissions_for_new_directories(), true); + } + + $quizData = $this->getQuizData($quizId, $sectionId); + + $this->createQuizXml($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); + $this->createModuleXml($quizData, $quizDir); + } + + /** + * Retrieves the quiz data. + * + * @param int $quizId The ID of the quiz. + * @param int $sectionId The ID of the section. + * + * @return array Quiz data. + */ + public function getQuizData($quizId, $sectionId) + { + $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, + ]; + } + } + + return null; + } + + /** + * Gets the section ID for a given quiz ID. + * + * @param int $quizId The ID of the quiz. + * + * @return int The section ID or 0 if not found. + */ + public function getSectionIdForQuiz($quizId) + { + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { + foreach ($learnpath->items as $item) { + if ($item['item_type'] == 'quiz' && $item['path'] == $quizId) { + return $learnpath->source_id; + } + } + } + + return 0; + } + + /** + * Retrieves the questions for a specific quiz. + * + * @param int $quizId The ID of the quiz. + * + * @return array List of questions related to the quiz. + */ + private function getQuestionsForQuiz($quizId) + { + $questions = []; + $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; // Get quiz questions + + // Loop through quiz questions + foreach ($quizResources as $questionId => $questionData) { + // Check if question is part of the quiz + if (in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids)) { + $questions[] = [ + 'id' => $questionData->source_id ?? $questionId, // Question ID + 'questiontext' => $questionData->question ?? '', // Question text + 'qtype' => $this->mapQuizType($questionData->quiz_type ?? '0'), // Question type + 'questioncategoryid' => $questionData->question_category ?? 0, // Category ID + 'answers' => $this->getAnswersForQuestion($questionData->source_id ?? $questionId), // Answers + 'maxmark' => $questionData->ponderation ?? 0, // Maximum mark + ]; + } + } + + return $questions; + } + + /** + * Retrieves the answers for a specific question ID. + * + * @param int $questionId The ID of the question. + * + * @return array List of answers for the question. + */ + private function getAnswersForQuestion($questionId) + { + $answers = []; + $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; // Get quiz questions + + // Loop through quiz resources + foreach ($quizResources as $questionData) { + // Check if the question ID matches + if ($questionData->source_id == $questionId) { + foreach ($questionData->answers as $answer) { + $answers[] = [ + 'text' => $answer['answer'] ?? '', // Answer text + 'fraction' => $answer['correct'] == '1' ? 1.0000000 : 0.0000000, // Answer fraction + 'feedback' => $answer['comment'] ?? '', // Feedback text + ]; + } + } + } + + return $answers; + } + + + /** + * Maps the quiz type code to a descriptive string. + * + * @param string $quizType The code for the quiz type. + * + * @return string The descriptive quiz type. + */ + private function mapQuizType($quizType) + { + // Maps the quiz type code to a descriptive string. + switch ($quizType) { + case '1': + return 'multichoice'; // Multiple choice questions. + case '2': + return 'truefalse'; // True/false questions. + // Add other types as needed. + default: + return 'unknown'; // Unknown quiz type. + } + } + + /** + * Retrieves feedbacks for a specific quiz. + * + * @param int $quizId The ID of the quiz. + * @return array Feedbacks associated with the quiz. + */ + private function getFeedbacksForQuiz($quizId) + { + $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. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createQuizXml($quizData, $destinationDir) + { + $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')); + } + } + + /** + * Creates the module.xml file. + * + * @param array $quizData Quiz data. + * @param int $sectionId ID of the section related to the quiz. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createModuleXml($quizData, $destinationDir) + { + + $section = new SectionExport($this->course); + $learnpath = $section->getLearnpathById($quizData['sectionid']); + $sectionData = $section->getSectionData($learnpath); + + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' '.$quizData['modulename'].'' . PHP_EOL; + $xmlContent .= ' ' . $quizData['sectionid'] . '' . PHP_EOL; + $xmlContent .= ' '.$sectionData['number'] .'' . 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; + + $xmlFile = $destinationDir . '/module.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception('Error creating module.xml'); + } + } + + /** + * Creates the grades.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createGradesXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' 1' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($quizData['name']) . '' . PHP_EOL; + $xmlContent .= ' mod' . PHP_EOL; + $xmlContent .= ' quiz' . PHP_EOL; + $xmlContent .= ' ' . $quizData['id'] . '' . PHP_EOL; + $xmlContent .= ' 0' . PHP_EOL; + $xmlContent .= ' $@NULL@$' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' $@NULL@$' . PHP_EOL; + $xmlContent .= ' 1' . PHP_EOL; + $xmlContent .= ' ' . $quizData['grade'] . '' . 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.09091' . PHP_EOL; + $xmlContent .= ' 0' . PHP_EOL; + $xmlContent .= ' 3' . 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 .= ' ' . time() . '' . PHP_EOL; + $xmlContent .= ' ' . time() . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/grades.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingGradesXml')); + } + } + + /** + * Creates the completion.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCompletionXml($quizData, $destinationDir) + { + $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; + + $xmlFile = $destinationDir . '/completion.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingCompletionXml')); + } + } + + /** + * Creates the comments.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCommentsXml($quizData, $destinationDir) + { + $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; + + $xmlFile = $destinationDir . '/comments.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingCommentsXml')); + } + } + + /** + * Creates the competencies.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCompetenciesXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' Sample Competency' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/competencies.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingCompetenciesXml')); + } + } + + /** + * Creates the filters.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createFiltersXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' filter_example' . PHP_EOL; + $xmlContent .= ' 1' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/filters.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingFiltersXml')); + } + } + + /** + * Creates the grade_history.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createGradeHistoryXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $quizData['grade'] . '' . PHP_EOL; + $xmlContent .= ' ' . time() . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/grade_history.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingGradeHistoryXml')); + } + } + + /** + * Creates the inforef.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createInforefXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' /path/to/file.pdf' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/inforef.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingInforefXml')); + } + } + + /** + * Creates the roles.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createRolesXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' Professor' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/roles.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingRolesXml')); + } + } + + /** + * Creates the calendar.xml file. + * + * @param array $quizData Quiz data. + * @param string $destinationDir Directory where the XML will be saved. + * + * @return void + */ + private function createCalendarXml($quizData, $destinationDir) + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' Due Date' . PHP_EOL; + $xmlContent .= ' ' . time() . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + $xmlFile = $destinationDir . '/calendar.xml'; + if (file_put_contents($xmlFile, $xmlContent) === false) { + throw new Exception(get_lang('ErrorCreatingCalendarXml')); + } + } +} diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php new file mode 100644 index 00000000000..059ef155116 --- /dev/null +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -0,0 +1,172 @@ +course = $course; + } + + /** + * Export a section and its activities to the appropriate directory. + * + * @param int $sectionId The ID of the section. + * @param string $exportDir The main export directory (where `sections/` will be created). + */ + public function exportSection($sectionId, $exportDir) + { + $sectionDir = $exportDir . "/sections/section_{$sectionId}"; + + if (!is_dir($sectionDir)) { + mkdir($sectionDir, api_get_permissions_for_new_directories(), true); + } + + $learnpath = $this->getLearnpathById($sectionId); + + if ($learnpath === null) { + throw new Exception("Learnpath with ID $sectionId not found."); + } + + $sectionData = $this->getSectionData($learnpath); + + $this->createSectionXml($sectionData, $sectionDir); + $this->createInforefXml($sectionData, $sectionDir); + + foreach ($sectionData['activities'] as $activity) { + if ($activity['modulename'] === 'quiz') { + $quizExport = new QuizExport($this->course); + $quizExport->exportQuiz($activity['id'], $exportDir, $activity['moduleid'], $sectionId); + } + // Add more types of activities here if needed + } + } + + /** + * Get the learnpath object by its ID. + * + * @param int $sectionId The ID of the section (learnpath). + * @return object|null The learnpath object or null if not found. + */ + public function getLearnpathById($sectionId) + { + foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { + if ($learnpath->source_id == $sectionId) { + return $learnpath; + } + } + return null; + } + + /** + * Get the section data (hardcoded for now). + * + * @param int $sectionId The ID of the section. + * + * @return array Section data. + */ + public function getSectionData($learnpath) + { + 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 the section (this simulates retrieving activities). + * + * @param int $sectionId The ID of the section. + * + * @return array List of activities. + */ + private function getActivitiesForSection($learnpath) + { + $activities = []; + foreach ($learnpath->items as $item) { + switch ($item['item_type']) { + case 'quiz': + $quizId = $item['path']; + $sectionId = $learnpath->source_id; + $quizExport = new QuizExport($this->course); + $quizData = $quizExport->getQuizData($quizId, $sectionId); + + $activities[] = [ + 'id' => $quizData['id'], + 'moduleid' => $quizData['moduleid'], + 'type' => 'quiz', + 'modulename' => $quizData['modulename'], + 'name' => $quizData['name'], + ]; + break; + + // Add more cases here for other types of activities if needed + + default: + // Handle other types of activities if needed + break; + } + } + return $activities; + } + + /** + * Create the section.xml file. + * + * @param array $sectionData Section data. + * @param string $destinationDir Directory where the XML will be saved. + */ + private function createSectionXml($sectionData, $destinationDir) + { + $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. + * + * @param array $sectionData Section data. + * @param string $destinationDir Directory where the XML will be saved. + */ + private function createInforefXml($sectionData, $destinationDir) + { + $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); + } +} From dcbd8c167eb6f323901f86799e9e865dca2b07d1 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Mon, 9 Sep 2024 15:00:05 -0500 Subject: [PATCH 2/8] Updated export for Moodle with page, quiz types, documents files and folders --- main/coursecopy/export_moodle.php | 4 - main/inc/lib/document.lib.php | 60 +++ main/inc/lib/moodleexport/ActivityExport.php | 245 +++++++++ main/inc/lib/moodleexport/CourseExport.php | 92 +--- main/inc/lib/moodleexport/FileExport.php | 295 +++++++--- main/inc/lib/moodleexport/FolderExport.php | 122 +++++ main/inc/lib/moodleexport/MoodleExport.php | 451 +++++++--------- main/inc/lib/moodleexport/PageExport.php | 105 ++++ main/inc/lib/moodleexport/QuizExport.php | 534 +++++++------------ main/inc/lib/moodleexport/ResourceExport.php | 84 +++ main/inc/lib/moodleexport/SectionExport.php | 224 ++++++-- 11 files changed, 1394 insertions(+), 822 deletions(-) create mode 100644 main/inc/lib/moodleexport/ActivityExport.php create mode 100644 main/inc/lib/moodleexport/FolderExport.php create mode 100644 main/inc/lib/moodleexport/PageExport.php create mode 100644 main/inc/lib/moodleexport/ResourceExport.php diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php index 5753bd54fff..d0b33071152 100644 --- a/main/coursecopy/export_moodle.php +++ b/main/coursecopy/export_moodle.php @@ -99,12 +99,8 @@ 'post', api_get_self().'?'.api_get_cidreq() ); - $form->addElement('header', get_lang('SelectOptionForExport')); $form->addElement('radio', 'export_option', '', get_lang('CreateFullExport'), 'full_export'); $form->addElement('radio', 'export_option', '', get_lang('LetMeSelectItems'), 'select_items'); - - // Add Moodle version selection - $form->addElement('header', get_lang('SelectMoodleVersion')); $form->addElement('select', 'moodle_version', get_lang('MoodleVersion'), [ '3' => 'Moodle 3.x', '4' => 'Moodle 4.x', 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..96bcdadf5a4 --- /dev/null +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -0,0 +1,245 @@ +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 the files associated with the activity. + */ + protected function createInforefXml(array $files, string $directory): void + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + if (is_array($files)) { + if (isset($files['modulename']) && in_array($files['modulename'], ['quiz'])) { + $xmlContent .= ' 0' . PHP_EOL; + } else { + if (isset($files['id'])) { + $files = [$files]; + } + foreach ($files as $file) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $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(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/CourseExport.php b/main/inc/lib/moodleexport/CourseExport.php index 51e73221d43..f9aa7154523 100644 --- a/main/inc/lib/moodleexport/CourseExport.php +++ b/main/inc/lib/moodleexport/CourseExport.php @@ -1,4 +1,5 @@ createCourseXml($this->courseInfo, $courseDir); + $this->createCourseXml($courseDir); $this->createEnrolmentsXml($this->courseInfo['enrolments'] ?? [], $courseDir); $this->createInforefXml($courseDir); $this->createRolesXml($this->courseInfo['roles'] ?? [], $courseDir); @@ -55,21 +52,18 @@ public function exportCourse($exportDir) /** * Create course.xml based on the course data from MoodleExport. - * - * @param array $courseInfo The course data passed from MoodleExport. - * @param string $destinationDir The directory where the XML will be saved. */ - private function createCourseXml($courseInfo, $destinationDir) + private function createCourseXml(string $destinationDir): void { - $courseId = $courseInfo['id'] ?? 0; - $contextId = $courseInfo['id'] ?? 1; - $shortname = $courseInfo['code'] ?? 'Unknown Course'; - $fullname = $courseInfo['title'] ?? 'Unknown Fullname'; - $showgrades = $courseInfo['showgrades'] ?? 0; - $startdate = $courseInfo['startdate'] ?? time(); - $enddate = $courseInfo['enddate'] ?? time() + (60 * 60 * 24 * 365); - $visible = $courseInfo['visible'] ?? 1; - $enablecompletion = $courseInfo['enablecompletion'] ?? 0; + $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; @@ -91,11 +85,8 @@ private function createCourseXml($courseInfo, $destinationDir) /** * Create enrolments.xml based on the course data from MoodleExport. - * - * @param array $enrolmentsData The enrolments data passed from MoodleExport. - * @param string $destinationDir The directory where the XML will be saved. */ - private function createEnrolmentsXml($enrolmentsData, $destinationDir) + private function createEnrolmentsXml(array $enrolmentsData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -116,12 +107,8 @@ private function createEnrolmentsXml($enrolmentsData, $destinationDir) /** * Creates the inforef.xml file with file references and question categories. - * - * @param string $destinationDir The directory where the XML file will be saved. - * - * @return void */ - private function createInforefXml($destinationDir) + private function createInforefXml(string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -154,13 +141,8 @@ private function createInforefXml($destinationDir) /** * Creates the roles.xml file. - * - * @param array $rolesData Role data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createRolesXml($rolesData, $destinationDir) + private function createRolesXml(array $rolesData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -177,13 +159,8 @@ private function createRolesXml($rolesData, $destinationDir) /** * Creates the calendar.xml file. - * - * @param array $calendarData Calendar event data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createCalendarXml($calendarData, $destinationDir) + private function createCalendarXml(array $calendarData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -205,13 +182,8 @@ private function createCalendarXml($calendarData, $destinationDir) /** * Creates the comments.xml file. - * - * @param array $commentsData Comment data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createCommentsXml($commentsData, $destinationDir) + private function createCommentsXml(array $commentsData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -231,13 +203,8 @@ private function createCommentsXml($commentsData, $destinationDir) /** * Creates the competencies.xml file. - * - * @param array $competenciesData Competency data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createCompetenciesXml($competenciesData, $destinationDir) + private function createCompetenciesXml(array $competenciesData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -254,13 +221,8 @@ private function createCompetenciesXml($competenciesData, $destinationDir) /** * Creates the completiondefaults.xml file. - * - * @param array $completionData Completion data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createCompletionDefaultsXml($completionData, $destinationDir) + private function createCompletionDefaultsXml(array $completionData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -277,13 +239,8 @@ private function createCompletionDefaultsXml($completionData, $destinationDir) /** * Creates the contentbank.xml file. - * - * @param array $contentBankData Content bank data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createContentBankXml($contentBankData, $destinationDir) + private function createContentBankXml(array $contentBankData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -299,13 +256,8 @@ private function createContentBankXml($contentBankData, $destinationDir) /** * Creates the filters.xml file. - * - * @param array $filtersData Filter data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createFiltersXml($filtersData, $destinationDir) + private function createFiltersXml(array $filtersData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php index e97708f9421..2305cb44afc 100644 --- a/main/inc/lib/moodleexport/FileExport.php +++ b/main/inc/lib/moodleexport/FileExport.php @@ -1,10 +1,14 @@ course = $course; } /** - * Export the files and their metadata from files.xml. - * - * @param array $filesData The data from files.xml. - * @param string $exportDir The directory where the files will be stored. + * Export files and metadata from files.xml to the specified directory. */ - public function exportFiles($filesData, $exportDir) + public function exportFiles(array $filesData, string $exportDir): void { - // Directory for storing files $filesDir = $exportDir . '/files'; - // Check if the directory exists, if not, create it if (!is_dir($filesDir)) { mkdir($filesDir, api_get_permissions_for_new_directories(), true); } - // Create a placeholder index.html file to prevent an empty directory - $placeholderFile = $filesDir . '/index.html'; - file_put_contents($placeholderFile, ""); + // Create placeholder index.html + $this->createPlaceholderFile($filesDir); - // Create files.xml in the root export directory + // 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, ""); + } /** - * Create the files.xml based on the provided data. - * - * @param array $filesData The data from files.xml. - * @param string $destinationDir The directory where the files.xml will be stored (root export directory). + * 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($filesData, $destinationDir) + private function createFilesXml(array $filesData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; foreach ($filesData['files'] as $file) { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['contenthash']) . '' . PHP_EOL; - $xmlContent .= ' ' . $file['contextid'] . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['component']) . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['filearea']) . '' . PHP_EOL; - $xmlContent .= ' ' . $file['itemid'] . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['filepath']) . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['filename']) . '' . PHP_EOL; - $xmlContent .= ' ' . $file['userid'] . '' . PHP_EOL; - $xmlContent .= ' ' . $file['filesize'] . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['mimetype']) . '' . PHP_EOL; - $xmlContent .= ' ' . $file['status'] . '' . PHP_EOL; - $xmlContent .= ' ' . $file['timecreated'] . '' . PHP_EOL; - $xmlContent .= ' ' . $file['timemodified'] . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['source']) . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['author']) . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($file['license']) . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= $this->createFileXmlEntry($file); } $xmlContent .= '' . PHP_EOL; + file_put_contents($destinationDir . '/files.xml', $xmlContent); + } - $xmlFile = $destinationDir . '/files.xml'; - file_put_contents($xmlFile, $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 the file data for testing purposes. Later will be dynamic. - * - * @return array The file data related to activities. + * Get file data from course resources. This is for testing purposes. + */ + public function getFilesData(): array + { + $filesData = ['files' => []]; + + foreach ($this->course->resources[RESOURCE_DOCUMENT] as $document) { + $filesData = $this->processDocument($filesData, $document); + } + + 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 + { + $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' => api_get_user_id(), + '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 + { + $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' => api_get_user_id(), + '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. */ - public function getFilesData() + private function getMimeTypes(): array { return [ - 'files' => [ - [ - 'id' => 1, - 'contenthash' => 'abcd1234efgh5678ijkl', - 'contextid' => 26, - 'component' => 'mod_assign', - 'filearea' => 'submission_files', - 'itemid' => 3, - 'filepath' => '/', - 'filename' => 'assignment_submission_001.pdf', - 'userid' => 5, - 'filesize' => 204800, - 'mimetype' => 'application/pdf', - 'status' => 0, - 'timecreated' => time() - 3600, - 'timemodified' => time(), - 'source' => 'Original Submission', - 'author' => 'Student Name', - 'license' => 'allrightsreserved', - ], - [ - 'id' => 2, - 'contenthash' => 'ijkl1234mnop5678qrst', - 'contextid' => 26, - 'component' => 'mod_quiz', - 'filearea' => 'feedback_files', - 'itemid' => 7, - 'filepath' => '/', - 'filename' => 'quiz_feedback_001.pdf', - 'userid' => 6, - 'filesize' => 102400, - 'mimetype' => 'application/pdf', - 'status' => 0, - 'timecreated' => time() - 7200, - 'timemodified' => time(), - 'source' => 'Quiz Feedback', - 'author' => 'Teacher Name', - 'license' => 'allrightsreserved', - ], - ] + '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..8708e719e56 --- /dev/null +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -0,0 +1,122 @@ +prepareActivityDirectory($exportDir, 'folder', $moduleId); + + // Retrieve folder data + $folderData = $this->getFolderData($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($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 getFolderData(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; + } + + /** + * 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/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index f06e620d6cd..99d069f97e4 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -5,55 +5,51 @@ namespace moodleexport; use Exception; +use FillBlanks; use ZipArchive; use RecursiveIteratorIterator; use RecursiveDirectoryIterator; /** * Class MoodleExport. + * Handles the export of a Moodle course in .mbz format. * * @package moodleexport */ class MoodleExport { - private $course; - public function __construct($course) + /** + * Constructor to initialize the course object. + */ + public function __construct(object $course) { $this->course = $course; } /** - * Export a course in Moodle format (.mbz). - * - * @param int $courseId The ID of the course to be exported. - * @param string $exportDir The directory where the export will be created. - * @param string $version The version of Moodle (3 or 4). - * - * @return bool|string Returns the path of the exported file or false in case of error. - * @throws Exception If an error occurs during export. + * Export the Moodle course in .mbz format. */ - public function export($courseId, $exportDir, $version) + public function export(string $courseId, string $exportDir, string $version) { - // Temporary directory where the export will be saved $tempDir = api_get_path(SYS_ARCHIVE_PATH) . $exportDir; - // Create the export directory if it doesn't exist if (!is_dir($tempDir)) { if (!mkdir($tempDir, api_get_permissions_for_new_directories(), true)) { throw new Exception(get_lang('ErrorCreatingDirectory')); } } - // Get course information $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 @@ -74,25 +70,16 @@ public function export($courseId, $exportDir, $version) // Compress everything into a .mbz (ZIP) file $exportedFile = $this->createMbzFile($tempDir); - // Clean up temporary directory if necessary + // Clean up temporary directory $this->cleanupTempDir($tempDir); return $exportedFile; } - /** - * Export all root XML files for the course. - * - * This method generates XML files for various aspects of the course including badges, completion, gradebook, - * grade history, groups, outcomes, and questions. It dynamically retrieves activities and quizzes, and exports - * questions associated with each quiz. - * - * @param string $exportDir The directory where XML files will be saved. - * - * @return void + * Export root XML files such as badges, completion, gradebook, etc. */ - private function exportRootXmlFiles($exportDir) + private function exportRootXmlFiles(string $exportDir): void { $this->exportBadgesXml($exportDir); $this->exportCompletionXml($exportDir); @@ -101,9 +88,9 @@ private function exportRootXmlFiles($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); @@ -111,7 +98,6 @@ private function exportRootXmlFiles($exportDir) $questionsData[] = $quizData; } } - $this->exportQuestionsXml($questionsData, $exportDir); $this->exportRolesXml($exportDir); @@ -119,22 +105,18 @@ private function exportRootXmlFiles($exportDir) $this->exportUsersXml($exportDir); } - /** - * Creates the moodle_backup.xml file with activities, sections, and settings. - * - * @param string $destinationDir The directory where the XML will be saved. - * @param string $version The Moodle version (3 or 4). - * - * @return bool Returns true if the file was created successfully. + * Create the moodle_backup.xml file with the required course details. */ - private function createMoodleBackupXml($destinationDir, $version) + private function createMoodleBackupXml(string $destinationDir, string $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; @@ -229,53 +211,96 @@ private function createMoodleBackupXml($destinationDir, $version) $xmlContent .= ''; $xmlFile = $destinationDir . '/moodle_backup.xml'; - return file_put_contents($xmlFile, $xmlContent) !== false; + file_put_contents($xmlFile, $xmlContent) !== false; } /** - * Retrieve all sections from the course. - * - * This method fetches and returns sections from the course resources where the learnpath type is '1'. - * It uses the SectionExport class to get section data based on each learnpath. - * - * @return array An array of section data. + * Get all sections from the course. */ - private function getSections() + 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; } /** - * Retrieve all activities from the course. - * - * @return array An array of activities. + * Get all activities from the course. */ - private function getActivities() + private function getActivities(): array { $activities = []; foreach ($this->course->resources as $resourceType => $resources) { foreach ($resources as $resource) { - if ($resource->obj->iid > 0) { - if ($resourceType === RESOURCE_QUIZ) { - $quizExport = new QuizExport($this->course); + // Handle quizzes + if ($resourceType === RESOURCE_QUIZ && $resource->obj->iid > 0) { + $quizExport = new QuizExport($this->course); + $activities[] = [ + 'id' => $resource->obj->iid, + 'sectionid' => $quizExport->getSectionIdForActivity($resource->obj->iid, RESOURCE_QUIZ), + 'modulename' => 'quiz', + 'moduleid' => $resource->obj->iid, + 'title' => $resource->obj->title, + ]; + } + + if ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { + $document = \DocumentManager::get_document_data_by_id($resource->source_id, $this->course->code); + // Handle documents (HTML pages) + if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION)) { + $pageExport = new PageExport($this->course); $activities[] = [ - 'id' => $resource->obj->iid, - 'sectionid' => $quizExport->getSectionIdForQuiz($resource->obj->iid), - 'modulename' => 'quiz', - 'moduleid' => $resource->obj->iid, - 'title' => $resource->obj->title, + 'id' => $resource->source_id, + 'sectionid' => $pageExport->getSectionIdForActivity($resource->source_id, RESOURCE_DOCUMENT), + 'modulename' => 'page', + 'moduleid' => $resource->source_id, + 'title' => $document['title'], ]; + } else { + // Handle files (resources with file_type 'file') + if ($resourceType === RESOURCE_DOCUMENT && $resource->file_type === 'file') { + $resourceExport = new ResourceExport($this->course); + $activities[] = [ + 'id' => $resource->source_id, + 'sectionid' => $resourceExport->getSectionIdForActivity($resource->source_id, RESOURCE_DOCUMENT), + 'modulename' => 'resource', + 'moduleid' => $resource->source_id, + 'title' => $resource->title, + ]; + } + + // Handle folders + if ($resourceType === RESOURCE_DOCUMENT && $resource->file_type === 'folder') { + $folderExport = new FolderExport($this->course); + $activities[] = [ + 'id' => $resource->source_id, + 'sectionid' => $folderExport->getSectionIdForActivity($resource->source_id, RESOURCE_DOCUMENT), + 'modulename' => 'folder', + 'moduleid' => $resource->source_id, + 'title' => $resource->title, + ]; + } } - - // Add more cases for other types of resources as needed } } } @@ -283,111 +308,152 @@ private function getActivities() 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. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportBadgesXml($exportDir) + private function exportBadgesXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate badges $xmlContent .= ''; - $xmlFile = $exportDir . '/badges.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/badges.xml', $xmlContent); } /** - * Export completion data to XML file. - * - * @param string $exportDir Directory to save the XML file. + * Export course completion data to XML file. */ - private function exportCompletionXml($exportDir) + private function exportCompletionXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate completion data $xmlContent .= ''; - $xmlFile = $exportDir . '/completion.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/completion.xml', $xmlContent); } /** * Export gradebook data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportGradebookXml($exportDir) + private function exportGradebookXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate gradebook data $xmlContent .= ''; - $xmlFile = $exportDir . '/gradebook.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/gradebook.xml', $xmlContent); } /** * Export grade history data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportGradeHistoryXml($exportDir) + private function exportGradeHistoryXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate grade history data $xmlContent .= ''; - $xmlFile = $exportDir . '/grade_history.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/grade_history.xml', $xmlContent); } /** * Export groups data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportGroupsXml($exportDir) + private function exportGroupsXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate groups data $xmlContent .= ''; - $xmlFile = $exportDir . '/groups.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/groups.xml', $xmlContent); } /** * Export outcomes data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportOutcomesXml($exportDir) + private function exportOutcomesXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate outcomes data $xmlContent .= ''; - $xmlFile = $exportDir . '/outcomes.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/outcomes.xml', $xmlContent); } /** - * Export the questions to questions.xml. - * - * @param array $questionsData The data of the questions (from getQuizData). - * @param string $exportDir The directory where the XML will be saved. + * Export questions data to XML file. */ - private function exportQuestionsXml($questionsData, $exportDir) + public function exportQuestionsXml(array $questionsData, string $exportDir): void { + $quizExport = new QuizExport($this->course); $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Iterate through each quiz to extract questions foreach ($questionsData as $quiz) { - // Assuming each entry in $questionsData represents a quiz - $categoryId = $quiz['questions'][0]['questioncategoryid'] ?? 'default_category_id'; // Use a default value if category is not set + $categoryId = $quiz['questions'][0]['questioncategoryid'] ?? '0'; $xmlContent .= ' ' . PHP_EOL; $xmlContent .= ' Default for ' . htmlspecialchars($quiz['name'] ?? 'Unknown') . '' . PHP_EOL; @@ -402,59 +468,8 @@ private function exportQuestionsXml($questionsData, $exportDir) $xmlContent .= ' $@NULL@$' . PHP_EOL; $xmlContent .= ' ' . PHP_EOL; - // Extract each question from the quiz foreach ($quiz['questions'] as $question) { - $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($question['qtype'] ?? 'unknown') . '' . PHP_EOL; - $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' my.moodle3.com+' . time() . '+QUESTIONSTAMP' . PHP_EOL; - $xmlContent .= ' my.moodle3.com+' . time() . '+VERSIONSTAMP' . PHP_EOL; - $xmlContent .= ' 0' . PHP_EOL; - $xmlContent .= ' ' . time() . '' . PHP_EOL; - $xmlContent .= ' ' . time() . '' . PHP_EOL; - $xmlContent .= ' 2' . PHP_EOL; - $xmlContent .= ' 2' . PHP_EOL; - - // Check if the question type is multichoice and add answers - if ($question['qtype'] === 'multichoice') { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - foreach ($question['answers'] as $answer) { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($answer['text'] ?? 'No answer text') . '' . PHP_EOL; - $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' ' . ($answer['fraction'] ?? '0') . '' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($answer['feedback'] ?? '') . '' . PHP_EOL; - $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - } - $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 .= ' 0' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - } - - $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= $quizExport->exportQuestion($question); } $xmlContent .= ' ' . PHP_EOL; @@ -462,81 +477,47 @@ private function exportQuestionsXml($questionsData, $exportDir) } $xmlContent .= ''; - - $xmlFile = $exportDir . '/questions.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/questions.xml', $xmlContent); } /** * Export roles data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportRolesXml($exportDir) + private function exportRolesXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate roles data $xmlContent .= ''; - $xmlFile = $exportDir . '/roles.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/roles.xml', $xmlContent); } /** * Export scales data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportScalesXml($exportDir) + private function exportScalesXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate scales data $xmlContent .= ''; - $xmlFile = $exportDir . '/scales.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/scales.xml', $xmlContent); } /** * Export users data to XML file. - * - * @param string $exportDir Directory to save the XML file. */ - private function exportUsersXml($exportDir) + private function exportUsersXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Populate users data $xmlContent .= ''; - $xmlFile = $exportDir . '/users.xml'; - file_put_contents($xmlFile, $xmlContent); + file_put_contents($exportDir . '/users.xml', $xmlContent); } /** - * Exports the course sections. - * - * @param int $courseId The ID of the course. - * @param string $exportDir The directory where the sections will be saved. - * - * @return void + * Export the backup settings. */ - private function exportSections($exportDir) + private function exportBackupSettings(): array { - $sections = $this->getSections(); - foreach ($sections as $section) { - $sectionExport = new SectionExport($this->course); - $sectionExport->exportSection($section['id'], $exportDir); - } - } - - /** - * Export backup settings dynamically for the course. - * - * @return array - */ - private function exportBackupSettings() - { - // this should be pulled from a configuration return [ ['level' => 'root', 'name' => 'users', 'value' => '1'], ['level' => 'root', 'name' => 'anonymize', 'value' => '0'], @@ -544,76 +525,4 @@ private function exportBackupSettings() // Add more settings as needed ]; } - - /** - * Compresses the exported course into an .mbz (ZIP) file. - * - * @param string $sourceDir Directory to compress. - * - * @return string The path of the exported .mbz file. - * @throws Exception If an error occurs while creating the ZIP. - */ - private function createMbzFile($sourceDir) - { - $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 $name => $file) { - // Skip directories - if (!$file->isDir()) { - $filePath = $file->getRealPath(); - $relativePath = substr($filePath, strlen($sourceDir) + 1); - - // Add the file to the ZIP - if (!$zip->addFile($filePath, $relativePath)) { - throw new Exception(get_lang('ErrorAddingFileToZip') . ": $relativePath"); - } - } - } - - // Close the ZIP file - if (!$zip->close()) { - throw new Exception(get_lang('ErrorClosingZip')); - } - - return $zipFile; - } - - /** - * Cleans up the temporary directory used for export. - * - * @param string $dir Directory to delete. - * - * @return void - */ - private function cleanupTempDir($dir) - { - $this->recursiveDelete($dir); - } - - /** - * Recursively deletes a directory and its contents. - * - * @param string $dir Directory to delete. - * - * @return void - */ - private function recursiveDelete($dir) - { - $files = array_diff(scandir($dir), ['.', '..']); - foreach ($files as $file) { - $path = "$dir/$file"; - (is_dir($path)) ? $this->recursiveDelete($path) : unlink($path); - } - rmdir($dir); - } } diff --git a/main/inc/lib/moodleexport/PageExport.php b/main/inc/lib/moodleexport/PageExport.php new file mode 100644 index 00000000000..3f07c880d1d --- /dev/null +++ b/main/inc/lib/moodleexport/PageExport.php @@ -0,0 +1,105 @@ +prepareActivityDirectory($exportDir, 'page', $moduleId); + + // Retrieve page data + $pageData = $this->getPageData($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($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 getPageData(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(), + ]; + } + } + + 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 index 289ec72745c..b08f429c7ca 100644 --- a/main/inc/lib/moodleexport/QuizExport.php +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -5,42 +5,34 @@ namespace moodleexport; use Exception; +use FillBlanks; /** * Class QuizExport. * - * @package moodleexport + * Handles the export of quizzes within a course. */ -class QuizExport +class QuizExport extends ActivityExport { - - private $course; - - public function __construct($course) - { - $this->course = $course; - } - /** * Export a quiz to the specified directory. * - * @param int $quizId The ID of the quiz. + * @param int $activityId The ID of the quiz. * @param string $exportDir The directory where the quiz will be exported. * @param int $moduleId The ID of the module. * @param int $sectionId The ID of the section. */ - public function exportQuiz($quizId, $exportDir, $moduleId, $sectionId) + public function export($activityId, $exportDir, $moduleId, $sectionId): void { - // Directory where the quiz export will be saved (activities/quiz_XXX) - $quizDir = $exportDir . "/activities/quiz_{$moduleId}"; - - if (!is_dir($quizDir)) { - mkdir($quizDir, api_get_permissions_for_new_directories(), true); - } + // Prepare the directory where the quiz export will be saved + $quizDir = $this->prepareActivityDirectory($exportDir, 'quiz', $moduleId); - $quizData = $this->getQuizData($quizId, $sectionId); + // Retrieve quiz data + $quizData = $this->getQuizData($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); @@ -48,20 +40,14 @@ public function exportQuiz($quizId, $exportDir, $moduleId, $sectionId) $this->createFiltersXml($quizData, $quizDir); $this->createGradeHistoryXml($quizData, $quizDir); $this->createInforefXml($quizData, $quizDir); - $this->createRolesXml($quizData, $quizDir); + $this->createRolesXml($quizDir); $this->createCalendarXml($quizData, $quizDir); - $this->createModuleXml($quizData, $quizDir); } /** * Retrieves the quiz data. - * - * @param int $quizId The ID of the quiz. - * @param int $sectionId The ID of the section. - * - * @return array Quiz data. */ - public function getQuizData($quizId, $sectionId) + public function getQuizData(int $quizId, int $sectionId): array { $quizResources = $this->course->resources[RESOURCE_QUIZ]; @@ -119,52 +105,26 @@ public function getQuizData($quizId, $sectionId) } } - return null; - } - - /** - * Gets the section ID for a given quiz ID. - * - * @param int $quizId The ID of the quiz. - * - * @return int The section ID or 0 if not found. - */ - public function getSectionIdForQuiz($quizId) - { - foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { - foreach ($learnpath->items as $item) { - if ($item['item_type'] == 'quiz' && $item['path'] == $quizId) { - return $learnpath->source_id; - } - } - } - - return 0; + return []; } /** * Retrieves the questions for a specific quiz. - * - * @param int $quizId The ID of the quiz. - * - * @return array List of questions related to the quiz. */ - private function getQuestionsForQuiz($quizId) + private function getQuestionsForQuiz(int $quizId): array { $questions = []; - $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; // Get quiz questions + $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; - // Loop through quiz questions foreach ($quizResources as $questionId => $questionData) { - // Check if question is part of the quiz if (in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids)) { $questions[] = [ - 'id' => $questionData->source_id ?? $questionId, // Question ID - 'questiontext' => $questionData->question ?? '', // Question text - 'qtype' => $this->mapQuizType($questionData->quiz_type ?? '0'), // Question type - 'questioncategoryid' => $questionData->question_category ?? 0, // Category ID - 'answers' => $this->getAnswersForQuestion($questionData->source_id ?? $questionId), // Answers - 'maxmark' => $questionData->ponderation ?? 0, // Maximum mark + '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, ]; } } @@ -172,64 +132,48 @@ private function getQuestionsForQuiz($quizId) 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. - * - * @param int $questionId The ID of the question. - * - * @return array List of answers for the question. */ - private function getAnswersForQuestion($questionId) + private function getAnswersForQuestion(int $questionId): array { $answers = []; - $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; // Get quiz questions + $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; - // Loop through quiz resources foreach ($quizResources as $questionData) { - // Check if the question ID matches if ($questionData->source_id == $questionId) { foreach ($questionData->answers as $answer) { $answers[] = [ - 'text' => $answer['answer'] ?? '', // Answer text - 'fraction' => $answer['correct'] == '1' ? 1.0000000 : 0.0000000, // Answer fraction - 'feedback' => $answer['comment'] ?? '', // Feedback text + 'text' => $answer['answer'], + 'fraction' => $answer['correct'] == '1' ? 100 : 0, + 'feedback' => $answer['comment'], ]; } } } - return $answers; } - - /** - * Maps the quiz type code to a descriptive string. - * - * @param string $quizType The code for the quiz type. - * - * @return string The descriptive quiz type. - */ - private function mapQuizType($quizType) - { - // Maps the quiz type code to a descriptive string. - switch ($quizType) { - case '1': - return 'multichoice'; // Multiple choice questions. - case '2': - return 'truefalse'; // True/false questions. - // Add other types as needed. - default: - return 'unknown'; // Unknown quiz type. - } - } - /** * Retrieves feedbacks for a specific quiz. - * - * @param int $quizId The ID of the quiz. - * @return array Feedbacks associated with the quiz. */ - private function getFeedbacksForQuiz($quizId) + private function getFeedbacksForQuiz(int $quizId): array { $feedbacks = []; $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? []; @@ -249,13 +193,8 @@ private function getFeedbacksForQuiz($quizId) /** * Creates the quiz.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void */ - private function createQuizXml($quizData, $destinationDir) + private function createQuizXml(array $quizData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; @@ -365,293 +304,182 @@ private function createQuizXml($quizData, $destinationDir) } /** - * Creates the module.xml file. - * - * @param array $quizData Quiz data. - * @param int $sectionId ID of the section related to the quiz. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports a question in XML format. */ - private function createModuleXml($quizData, $destinationDir) + public function exportQuestion(array $question): string { - - $section = new SectionExport($this->course); - $learnpath = $section->getLearnpathById($quizData['sectionid']); - $sectionData = $section->getSectionData($learnpath); - - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' '.$quizData['modulename'].'' . PHP_EOL; - $xmlContent .= ' ' . $quizData['sectionid'] . '' . PHP_EOL; - $xmlContent .= ' '.$sectionData['number'] .'' . 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; - - $xmlFile = $destinationDir . '/module.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception('Error creating module.xml'); + $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 .= ' my.moodle3.com+' . time() . '+QUESTIONSTAMP' . PHP_EOL; + $xmlContent .= ' my.moodle3.com+' . 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; } - } - /** - * Creates the grades.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void - */ - private function createGradesXml($quizData, $destinationDir) - { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' ' . htmlspecialchars($quizData['name']) . '' . PHP_EOL; - $xmlContent .= ' mod' . PHP_EOL; - $xmlContent .= ' quiz' . PHP_EOL; - $xmlContent .= ' ' . $quizData['id'] . '' . PHP_EOL; - $xmlContent .= ' 0' . PHP_EOL; - $xmlContent .= ' $@NULL@$' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' $@NULL@$' . PHP_EOL; - $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' ' . $quizData['grade'] . '' . 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.09091' . PHP_EOL; - $xmlContent .= ' 0' . PHP_EOL; - $xmlContent .= ' 3' . 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 .= ' ' . time() . '' . PHP_EOL; - $xmlContent .= ' ' . time() . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - $xmlFile = $destinationDir . '/grades.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingGradesXml')); - } - } + $xmlContent .= ' ' . PHP_EOL; - /** - * Creates the completion.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void - */ - private function createCompletionXml($quizData, $destinationDir) - { - $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; - - $xmlFile = $destinationDir . '/completion.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingCompletionXml')); - } + return $xmlContent; } /** - * Creates the comments.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports a multiple-choice question in XML format. */ - private function createCommentsXml($quizData, $destinationDir) + private function exportMultichoiceQuestion(array $question): string { - $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; - - $xmlFile = $destinationDir . '/comments.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingCommentsXml')); + $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; } /** - * Creates the competencies.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports a multiple-choice question with single=0 in XML format. */ - private function createCompetenciesXml($quizData, $destinationDir) + private function exportMultichoiceNosingleQuestion(array $question): string { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' Sample Competency' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - $xmlFile = $destinationDir . '/competencies.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingCompetenciesXml')); - } + // Similar structure to exportMultichoiceQuestion, but with single=0 + $xmlContent = str_replace('1', '0', $this->exportMultichoiceQuestion($question)); + return $xmlContent; } /** - * Creates the filters.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports a true/false question in XML format. */ - private function createFiltersXml($quizData, $destinationDir) + private function exportTrueFalseQuestion(array $question): string { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' filter_example' . PHP_EOL; - $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - $xmlFile = $destinationDir . '/filters.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingFiltersXml')); + $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; } /** - * Creates the grade_history.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports a short answer question in XML format. */ - private function createGradeHistoryXml($quizData, $destinationDir) + private function exportShortAnswerQuestion(array $question): string { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $quizData['grade'] . '' . PHP_EOL; - $xmlContent .= ' ' . time() . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - $xmlFile = $destinationDir . '/grade_history.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingGradeHistoryXml')); + $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; } /** - * Creates the inforef.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports a matching question in XML format. */ - private function createInforefXml($quizData, $destinationDir) + private function exportMatchQuestion(array $question): string { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' /path/to/file.pdf' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - $xmlFile = $destinationDir . '/inforef.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingInforefXml')); + $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; + } } - } - /** - * Creates the roles.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void - */ - private function createRolesXml($quizData, $destinationDir) - { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' Professor' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; - $xmlFile = $destinationDir . '/roles.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingRolesXml')); - } + return $xmlContent; } /** - * Creates the calendar.xml file. - * - * @param array $quizData Quiz data. - * @param string $destinationDir Directory where the XML will be saved. - * - * @return void + * Exports an answer in XML format. */ - private function createCalendarXml($quizData, $destinationDir) + private function exportAnswer(array $answer): string { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' Due Date' . PHP_EOL; - $xmlContent .= ' ' . time() . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - $xmlFile = $destinationDir . '/calendar.xml'; - if (file_put_contents($xmlFile, $xmlContent) === false) { - throw new Exception(get_lang('ErrorCreatingCalendarXml')); - } + 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..465620976bf --- /dev/null +++ b/main/inc/lib/moodleexport/ResourceExport.php @@ -0,0 +1,84 @@ +prepareActivityDirectory($exportDir, 'resource', $moduleId); + + // Retrieve resource data + $resourceData = $this->getResourceData($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($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); + } + + /** + * Get resource data dynamically from the course. + */ + public function getResourceData(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(), + ]; + } +} diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index 059ef155116..90b2dce7c37 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -8,6 +8,7 @@ /** * Class SectionExport. + * Handles the export of course sections and their activities. * * @package moodleexport */ @@ -15,18 +16,20 @@ class SectionExport { private $course; + /** + * Constructor to initialize the course object. + * + * @param object $course The course object to be exported. + */ public function __construct($course) { $this->course = $course; } /** - * Export a section and its activities to the appropriate directory. - * - * @param int $sectionId The ID of the section. - * @param string $exportDir The main export directory (where `sections/` will be created). + * Export a section and its activities to the specified directory. */ - public function exportSection($sectionId, $exportDir) + public function exportSection(int $sectionId, string $exportDir): void { $sectionDir = $exportDir . "/sections/section_{$sectionId}"; @@ -34,50 +37,144 @@ public function exportSection($sectionId, $exportDir) mkdir($sectionDir, api_get_permissions_for_new_directories(), true); } - $learnpath = $this->getLearnpathById($sectionId); - - if ($learnpath === null) { - throw new Exception("Learnpath with ID $sectionId not found."); + 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(), + ]; } - $sectionData = $this->getSectionData($learnpath); - $this->createSectionXml($sectionData, $sectionDir); $this->createInforefXml($sectionData, $sectionDir); + $this->exportActivities($sectionData['activities'], $exportDir, $sectionId); + } - foreach ($sectionData['activities'] as $activity) { - if ($activity['modulename'] === 'quiz') { - $quizExport = new QuizExport($this->course); - $quizExport->exportQuiz($activity['id'], $exportDir, $activity['moduleid'], $sectionId); + /** + * Export the activities of a section. + */ + private function exportActivities(array $activities, string $exportDir, int $sectionId): void + { + foreach ($activities as $activity) { + switch ($activity['modulename']) { + case 'quiz': + $quizExport = new QuizExport($this->course); + $quizExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); + break; + case 'page': + $pageExport = new PageExport($this->course); + $pageExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); + break; + case 'resource': + $resourceExport = new ResourceExport($this->course); + $resourceExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); + break; + case 'folder': + $folderExport = new FolderExport($this->course); + $folderExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); + break; + } + } + } + + /** + * Get all general items not linked to any lesson (learnpath). + */ + public function getGeneralItems(): array + { + $generalItems = []; + + if (!empty($this->course->resources[RESOURCE_DOCUMENT])) { + foreach ($this->course->resources[RESOURCE_DOCUMENT] as $document) { + if (!$this->isItemInLearnpath($document, RESOURCE_DOCUMENT)) { + $generalItems[] = [ + 'id' => $document->source_id, + 'item_type' => 'document', + 'path' => $document->source_id, + 'title' => $document->title, + ]; + } + } + } + + if (!empty($this->course->resources[RESOURCE_QUIZ])) { + foreach ($this->course->resources[RESOURCE_QUIZ] as $id => $quiz) { + if (!$this->isItemInLearnpath($quiz, RESOURCE_QUIZ)) { + $generalItems[] = [ + 'id' => $quiz->source_id, + 'item_type' => 'quiz', + 'path' => $id, + 'title' => $quiz->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; + } + } + } } - // Add more types of activities here if needed } + + return false; } /** * Get the learnpath object by its ID. - * - * @param int $sectionId The ID of the section (learnpath). - * @return object|null The learnpath object or null if not found. */ - public function getLearnpathById($sectionId) + public function getLearnpathById(int $sectionId): ?object { foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { if ($learnpath->source_id == $sectionId) { return $learnpath; } } + return null; } /** - * Get the section data (hardcoded for now). - * - * @param int $sectionId The ID of the section. - * - * @return array Section data. + * Get section data for a learnpath. */ - public function getSectionData($learnpath) + public function getSectionData(object $learnpath): array { return [ 'id' => $learnpath->source_id, @@ -92,20 +189,17 @@ public function getSectionData($learnpath) } /** - * Get the activities for the section (this simulates retrieving activities). - * - * @param int $sectionId The ID of the section. - * - * @return array List of activities. + * Get the activities for a specific section. */ - private function getActivitiesForSection($learnpath) + public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array { $activities = []; + $sectionId = $isGeneral ? 0 : $learnpath->source_id; + foreach ($learnpath->items as $item) { switch ($item['item_type']) { case 'quiz': - $quizId = $item['path']; - $sectionId = $learnpath->source_id; + $quizId = (int) $item['path']; $quizExport = new QuizExport($this->course); $quizData = $quizExport->getQuizData($quizId, $sectionId); @@ -118,23 +212,66 @@ private function getActivitiesForSection($learnpath) ]; break; - // Add more cases here for other types of activities if needed + case 'document': + $documentId = (int) $item['path']; + $document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code); + + // Handle HTML files (pages) + if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION)) { + $pageId = $item['path']; + $pageExport = new PageExport($this->course); + $pageData = $pageExport->getPageData($pageId, $sectionId); + + $activities[] = [ + 'id' => $pageData['id'], + 'moduleid' => $pageData['moduleid'], + 'type' => 'page', + 'modulename' => 'page', + 'name' => $pageData['name'], + ]; + } + // Handle file-type documents (resources) + elseif ('file' === $document['filetype']) { + $resourceId = $item['path']; + $resourceExport = new ResourceExport($this->course); + $resourceData = $resourceExport->getResourceData($resourceId, $sectionId); + + $activities[] = [ + 'id' => $resourceData['id'], + 'moduleid' => $resourceData['moduleid'], + 'type' => 'resource', + 'modulename' => 'resource', + 'name' => $resourceData['name'], + ]; + } + // Handle folder-type documents + elseif ('folder' === $document['filetype']) { + $folderId = $item['path']; + $folderExport = new FolderExport($this->course); + $folderData = $folderExport->getFolderData($folderId, $sectionId); + + $activities[] = [ + 'id' => $folderData['id'], + 'moduleid' => $folderData['moduleid'], + 'type' => 'folder', + 'modulename' => 'folder', + 'name' => $folderData['name'], + ]; + } + break; default: - // Handle other types of activities if needed break; } } + return $activities; } /** * Create the section.xml file. - * - * @param array $sectionData Section data. - * @param string $destinationDir Directory where the XML will be saved. */ - private function createSectionXml($sectionData, $destinationDir) + private function createSectionXml(array $sectionData, string $destinationDir): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '
    ' . PHP_EOL; @@ -153,17 +290,16 @@ private function createSectionXml($sectionData, $destinationDir) /** * Create the inforef.xml file for the section. - * - * @param array $sectionData Section data. - * @param string $destinationDir Directory where the XML will be saved. */ - private function createInforefXml($sectionData, $destinationDir) + 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'; From f758557222fe492e4493f2882000f6dc1d136e19 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 10 Sep 2024 18:27:28 -0500 Subject: [PATCH 3/8] Updated export for Moodle with url, glossary and assignments --- main/coursecopy/export_moodle.php | 2 + main/inc/lib/moodleexport/ActivityExport.php | 41 ++-- main/inc/lib/moodleexport/AssignExport.php | 220 +++++++++++++++++++ main/inc/lib/moodleexport/CourseExport.php | 2 +- main/inc/lib/moodleexport/FileExport.php | 33 +++ main/inc/lib/moodleexport/FolderExport.php | 6 +- main/inc/lib/moodleexport/GlossaryExport.php | 141 ++++++++++++ main/inc/lib/moodleexport/MoodleExport.php | 105 +++++---- main/inc/lib/moodleexport/PageExport.php | 6 +- main/inc/lib/moodleexport/QuizExport.php | 6 +- main/inc/lib/moodleexport/ResourceExport.php | 6 +- main/inc/lib/moodleexport/SectionExport.php | 214 +++++++++--------- main/inc/lib/moodleexport/UrlExport.php | 84 +++++++ 13 files changed, 691 insertions(+), 175 deletions(-) create mode 100644 main/inc/lib/moodleexport/AssignExport.php create mode 100644 main/inc/lib/moodleexport/GlossaryExport.php create mode 100644 main/inc/lib/moodleexport/UrlExport.php diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php index d0b33071152..1030287a0b2 100644 --- a/main/coursecopy/export_moodle.php +++ b/main/coursecopy/export_moodle.php @@ -11,6 +11,8 @@ * */ require_once __DIR__.'/../inc/global.inc.php'; +require_once api_get_path(SYS_PATH).'main/work/work.lib.php'; + $current_course_tool = TOOL_COURSE_MAINTENANCE; api_protect_course_script(true); diff --git a/main/inc/lib/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php index 96bcdadf5a4..e10ede1477e 100644 --- a/main/inc/lib/moodleexport/ActivityExport.php +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -116,22 +116,35 @@ protected function createInforefXml(array $files, string $directory): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - if (is_array($files)) { - if (isset($files['modulename']) && in_array($files['modulename'], ['quiz'])) { - $xmlContent .= ' 0' . PHP_EOL; - } else { - if (isset($files['id'])) { - $files = [$files]; - } - foreach ($files as $file) { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $file['id'] . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; + + // Handle different module types + if (isset($files['modulename']) && $files['modulename'] === 'glossary') { + // For glossary, include user references + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $files['userid'] . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } else { + // Default handling for other modules (e.g., quiz, documents) + $xmlContent .= ' ' . PHP_EOL; + if (is_array($files)) { + if (isset($files['modulename']) && in_array($files['modulename'], ['quiz'])) { + $xmlContent .= ' 0' . PHP_EOL; + } else { + if (isset($files['id'])) { + $files = [$files]; + } + foreach ($files as $file) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $file['id'] . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } } } + $xmlContent .= ' ' . PHP_EOL; } - $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; $this->createXmlFile('inforef', $xmlContent, $directory); @@ -140,7 +153,7 @@ protected function createInforefXml(array $files, string $directory): void /** * Creates the roles.xml file. */ - protected function createRolesXml(string $directory): void + protected function createRolesXml(array $activityData, string $directory): void { $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; diff --git a/main/inc/lib/moodleexport/AssignExport.php b/main/inc/lib/moodleexport/AssignExport.php new file mode 100644 index 00000000000..01d6b5a5381 --- /dev/null +++ b/main/inc/lib/moodleexport/AssignExport.php @@ -0,0 +1,220 @@ +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); + } + + /** + * 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); + } + + /** + * Create the inforef.xml file for the assign activity. + * This method can be customized to handle attachments. + */ + protected function createInforefXml(array $files, string $directory): void + { + $xmlContent = '' . PHP_EOL; + $xmlContent .= '' . PHP_EOL; + + if (!empty($files['files'])) { + $xmlContent .= ' ' . PHP_EOL; + foreach ($files['files'] as $file) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $file['id'] . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } + $xmlContent .= ' ' . PHP_EOL; + } + + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $files['grade_item_id'] . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + + $xmlContent .= ''; + + $this->createXmlFile('inforef', $xmlContent, $directory); + } + + + /** + * 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'])), + ]; + } + } + } + + 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, + 'area_id' => 0 + ]; + } +} diff --git a/main/inc/lib/moodleexport/CourseExport.php b/main/inc/lib/moodleexport/CourseExport.php index f9aa7154523..f2976fc848e 100644 --- a/main/inc/lib/moodleexport/CourseExport.php +++ b/main/inc/lib/moodleexport/CourseExport.php @@ -117,7 +117,7 @@ private function createInforefXml(string $destinationDir): void foreach ($this->activities as $activity) { if ($activity['modulename'] === 'quiz') { $quizExport = new QuizExport($this->course); - $quizData = $quizExport->getQuizData($activity['id'], $activity['sectionid']); + $quizData = $quizExport->getData($activity['id'], $activity['sectionid']); foreach ($quizData['questions'] as $question) { $categoryId = $question['questioncategoryid']; if (!in_array($categoryId, $questionCategories, true)) { diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php index 2305cb44afc..42f6c7f386a 100644 --- a/main/inc/lib/moodleexport/FileExport.php +++ b/main/inc/lib/moodleexport/FileExport.php @@ -4,6 +4,7 @@ namespace moodleexport; +use DocumentManager; use Exception; /** @@ -142,6 +143,38 @@ public function getFilesData(): array $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' => api_get_user_id(), + 'filesize' => $docData['size'], + 'mimetype' => $this->getMimeType($docData['path']), + 'status' => 0, + 'timecreated' => time() - 3600, + 'timemodified' => time(), + 'source' => $docData['title'], + 'author' => 'Unknown', + 'license' => 'allrightsreserved', + ]; + } + } + } + } + return $filesData; } diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php index 8708e719e56..c8c2a744c2b 100644 --- a/main/inc/lib/moodleexport/FolderExport.php +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -25,7 +25,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $folderDir = $this->prepareActivityDirectory($exportDir, 'folder', $moduleId); // Retrieve folder data - $folderData = $this->getFolderData($activityId, $sectionId); + $folderData = $this->getData($activityId, $sectionId); // Generate XML files $this->createFolderXml($folderData, $folderDir); @@ -34,7 +34,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createFiltersXml($folderData, $folderDir); $this->createGradeHistoryXml($folderData, $folderDir); $this->createInforefXml($this->getFilesForFolder($activityId), $folderDir); - $this->createRolesXml($folderDir); + $this->createRolesXml($folderData, $folderDir); } /** @@ -63,7 +63,7 @@ private function createFolderXml(array $folderData, string $folderDir): void /** * Get folder data dynamically from the course. */ - public function getFolderData(int $folderId, int $sectionId): array + public function getData(int $folderId, int $sectionId): array { $folder = $this->course->resources['document'][$folderId]; diff --git a/main/inc/lib/moodleexport/GlossaryExport.php b/main/inc/lib/moodleexport/GlossaryExport.php new file mode 100644 index 00000000000..f815663b79b --- /dev/null +++ b/main/inc/lib/moodleexport/GlossaryExport.php @@ -0,0 +1,141 @@ +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 .= ' ' . $entry['definition'] . '>' . 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 + { + $glossaryEntries = []; + foreach ($this->course->resources['glossary'] as $glossary) { + $glossaryEntries[] = [ + 'id' => $glossary->glossary_id, + 'userid' => api_get_user_id(), + 'concept' => 'Concept', + 'definition' => 'Definition', + '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' => api_get_user_id(), + 'entries' => $glossaryEntries, + ]; + } +} diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index 99d069f97e4..64dc27b746d 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -94,7 +94,7 @@ private function exportRootXmlFiles(string $exportDir): void foreach ($activities as $activity) { if ($activity['modulename'] === 'quiz') { $quizExport = new QuizExport($this->course); - $quizData = $quizExport->getQuizData($activity['id'], $activity['sectionid']); + $quizData = $quizExport->getData($activity['id'], $activity['sectionid']); $questionsData[] = $quizData; } } @@ -249,59 +249,76 @@ private function getSections(): array 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) { - $quizExport = new QuizExport($this->course); - $activities[] = [ - 'id' => $resource->obj->iid, - 'sectionid' => $quizExport->getSectionIdForActivity($resource->obj->iid, RESOURCE_QUIZ), - 'modulename' => 'quiz', - 'moduleid' => $resource->obj->iid, - 'title' => $resource->obj->title, - ]; + $exportClass = QuizExport::class; + $moduleName = 'quiz'; + $id = $resource->obj->iid; + $title = $resource->obj->title; } - - if ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { + // 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 documents (HTML pages) + elseif ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) { $document = \DocumentManager::get_document_data_by_id($resource->source_id, $this->course->code); - // Handle documents (HTML pages) if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION)) { - $pageExport = new PageExport($this->course); - $activities[] = [ - 'id' => $resource->source_id, - 'sectionid' => $pageExport->getSectionIdForActivity($resource->source_id, RESOURCE_DOCUMENT), - 'modulename' => 'page', - 'moduleid' => $resource->source_id, - 'title' => $document['title'], - ]; - } else { - // Handle files (resources with file_type 'file') - if ($resourceType === RESOURCE_DOCUMENT && $resource->file_type === 'file') { - $resourceExport = new ResourceExport($this->course); - $activities[] = [ - 'id' => $resource->source_id, - 'sectionid' => $resourceExport->getSectionIdForActivity($resource->source_id, RESOURCE_DOCUMENT), - 'modulename' => 'resource', - 'moduleid' => $resource->source_id, - 'title' => $resource->title, - ]; - } - - // Handle folders - if ($resourceType === RESOURCE_DOCUMENT && $resource->file_type === 'folder') { - $folderExport = new FolderExport($this->course); - $activities[] = [ - 'id' => $resource->source_id, - 'sectionid' => $folderExport->getSectionIdForActivity($resource->source_id, RESOURCE_DOCUMENT), - 'modulename' => 'folder', - 'moduleid' => $resource->source_id, - 'title' => $resource->title, - ]; - } + $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'] ?? ''; + } + + // 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, + ]; + } } } diff --git a/main/inc/lib/moodleexport/PageExport.php b/main/inc/lib/moodleexport/PageExport.php index 3f07c880d1d..a13ee108e91 100644 --- a/main/inc/lib/moodleexport/PageExport.php +++ b/main/inc/lib/moodleexport/PageExport.php @@ -25,7 +25,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $pageDir = $this->prepareActivityDirectory($exportDir, 'page', $moduleId); // Retrieve page data - $pageData = $this->getPageData($activityId, $sectionId); + $pageData = $this->getData($activityId, $sectionId); // Generate XML files $this->createPageXml($pageData, $pageDir); @@ -34,7 +34,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createFiltersXml($pageData, $pageDir); $this->createGradeHistoryXml($pageData, $pageDir); $this->createInforefXml($pageData, $pageDir); - $this->createRolesXml($pageDir); + $this->createRolesXml($pageData, $pageDir); } /** @@ -64,7 +64,7 @@ private function createPageXml(array $pageData, string $pageDir): void /** * Get page data dynamically from the course. */ - public function getPageData(int $pageId, int $sectionId): ?array + public function getData(int $pageId, int $sectionId): ?array { $pageResources = $this->course->resources[RESOURCE_DOCUMENT]; diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php index b08f429c7ca..f05d6848c4b 100644 --- a/main/inc/lib/moodleexport/QuizExport.php +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -28,7 +28,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $quizDir = $this->prepareActivityDirectory($exportDir, 'quiz', $moduleId); // Retrieve quiz data - $quizData = $this->getQuizData($activityId, $sectionId); + $quizData = $this->getData($activityId, $sectionId); // Generate XML files $this->createQuizXml($quizData, $quizDir); @@ -40,14 +40,14 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createFiltersXml($quizData, $quizDir); $this->createGradeHistoryXml($quizData, $quizDir); $this->createInforefXml($quizData, $quizDir); - $this->createRolesXml($quizDir); + $this->createRolesXml($quizData, $quizDir); $this->createCalendarXml($quizData, $quizDir); } /** * Retrieves the quiz data. */ - public function getQuizData(int $quizId, int $sectionId): array + public function getData(int $quizId, int $sectionId): array { $quizResources = $this->course->resources[RESOURCE_QUIZ]; diff --git a/main/inc/lib/moodleexport/ResourceExport.php b/main/inc/lib/moodleexport/ResourceExport.php index 465620976bf..6509cf8fa37 100644 --- a/main/inc/lib/moodleexport/ResourceExport.php +++ b/main/inc/lib/moodleexport/ResourceExport.php @@ -25,7 +25,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $resourceDir = $this->prepareActivityDirectory($exportDir, 'resource', $moduleId); // Retrieve resource data - $resourceData = $this->getResourceData($activityId, $sectionId); + $resourceData = $this->getData($activityId, $sectionId); // Generate XML files $this->createResourceXml($resourceData, $resourceDir); @@ -34,7 +34,7 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createFiltersXml($resourceData, $resourceDir); $this->createGradeHistoryXml($resourceData, $resourceDir); $this->createInforefXml($resourceData, $resourceDir); - $this->createRolesXml($resourceDir); + $this->createRolesXml($resourceData, $resourceDir); } /** @@ -65,7 +65,7 @@ private function createResourceXml(array $resourceData, string $resourceDir): vo /** * Get resource data dynamically from the course. */ - public function getResourceData(int $resourceId, int $sectionId): array + public function getData(int $resourceId, int $sectionId): array { $resource = $this->course->resources[RESOURCE_DOCUMENT][$resourceId]; diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index 90b2dce7c37..e1f19ad5cd0 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -66,24 +66,23 @@ public function exportSection(int $sectionId, string $exportDir): void */ private function exportActivities(array $activities, string $exportDir, int $sectionId): void { + $exportClasses = [ + 'quiz' => QuizExport::class, + 'glossary' => GlossaryExport::class, + 'url' => UrlExport::class, + 'assign' => AssignExport::class, + 'page' => PageExport::class, + 'resource' => ResourceExport::class, + 'folder' => FolderExport::class, + ]; + foreach ($activities as $activity) { - switch ($activity['modulename']) { - case 'quiz': - $quizExport = new QuizExport($this->course); - $quizExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); - break; - case 'page': - $pageExport = new PageExport($this->course); - $pageExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); - break; - case 'resource': - $resourceExport = new ResourceExport($this->course); - $resourceExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); - break; - case 'folder': - $folderExport = new FolderExport($this->course); - $folderExport->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); - break; + $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."); } } } @@ -95,28 +94,29 @@ public function getGeneralItems(): array { $generalItems = []; - if (!empty($this->course->resources[RESOURCE_DOCUMENT])) { - foreach ($this->course->resources[RESOURCE_DOCUMENT] as $document) { - if (!$this->isItemInLearnpath($document, RESOURCE_DOCUMENT)) { - $generalItems[] = [ - 'id' => $document->source_id, - 'item_type' => 'document', - 'path' => $document->source_id, - 'title' => $document->title, - ]; - } - } - } + // 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', + ]; - if (!empty($this->course->resources[RESOURCE_QUIZ])) { - foreach ($this->course->resources[RESOURCE_QUIZ] as $id => $quiz) { - if (!$this->isItemInLearnpath($quiz, RESOURCE_QUIZ)) { - $generalItems[] = [ - 'id' => $quiz->source_id, - 'item_type' => 'quiz', - 'path' => $id, - 'title' => $quiz->title, - ]; + 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, + ]; + } } } } @@ -197,77 +197,83 @@ public function getActivitiesForSection(object $learnpath, bool $isGeneral = fal $sectionId = $isGeneral ? 0 : $learnpath->source_id; foreach ($learnpath->items as $item) { - switch ($item['item_type']) { - case 'quiz': - $quizId = (int) $item['path']; - $quizExport = new QuizExport($this->course); - $quizData = $quizExport->getQuizData($quizId, $sectionId); - - $activities[] = [ - 'id' => $quizData['id'], - 'moduleid' => $quizData['moduleid'], - 'type' => 'quiz', - 'modulename' => $quizData['modulename'], - 'name' => $quizData['name'], - ]; - break; - - case 'document': - $documentId = (int) $item['path']; - $document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code); - - // Handle HTML files (pages) - if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION)) { - $pageId = $item['path']; - $pageExport = new PageExport($this->course); - $pageData = $pageExport->getPageData($pageId, $sectionId); - - $activities[] = [ - 'id' => $pageData['id'], - 'moduleid' => $pageData['moduleid'], - 'type' => 'page', - 'modulename' => 'page', - 'name' => $pageData['name'], - ]; - } - // Handle file-type documents (resources) - elseif ('file' === $document['filetype']) { - $resourceId = $item['path']; - $resourceExport = new ResourceExport($this->course); - $resourceData = $resourceExport->getResourceData($resourceId, $sectionId); - - $activities[] = [ - 'id' => $resourceData['id'], - 'moduleid' => $resourceData['moduleid'], - 'type' => 'resource', - 'modulename' => 'resource', - 'name' => $resourceData['name'], - ]; - } - // Handle folder-type documents - elseif ('folder' === $document['filetype']) { - $folderId = $item['path']; - $folderExport = new FolderExport($this->course); - $folderData = $folderExport->getFolderData($folderId, $sectionId); - - $activities[] = [ - 'id' => $folderData['id'], - 'moduleid' => $folderData['moduleid'], - 'type' => 'folder', - 'modulename' => 'folder', - 'name' => $folderData['name'], - ]; - } - break; - - default: - break; - } + $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, + 'page' => PageExport::class, + 'resource' => ResourceExport::class, + 'folder' => FolderExport::class, + ]; + + $itemType = $item['item_type'] === 'link' ? 'url' : ($item['item_type'] === 'work' ? 'assign' : $item['item_type']); + + switch ($itemType) { + case 'quiz': + case 'glossary': + case 'assign': + case 'url': + $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. */ diff --git a/main/inc/lib/moodleexport/UrlExport.php b/main/inc/lib/moodleexport/UrlExport.php new file mode 100644 index 00000000000..8f281b3425e --- /dev/null +++ b/main/inc/lib/moodleexport/UrlExport.php @@ -0,0 +1,84 @@ +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); + } + + /** + * 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, + ]; + } +} From e2875ac625da77a665fbad7a88192e600bbb04d8 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Thu, 12 Sep 2024 23:34:17 -0500 Subject: [PATCH 4/8] Add admin form handling, forum export, and code improvements --- main/coursecopy/export_moodle.php | 175 +++++++------ main/inc/lib/moodleexport/ActivityExport.php | 41 ++-- main/inc/lib/moodleexport/AssignExport.php | 38 +-- main/inc/lib/moodleexport/CourseExport.php | 62 ++++- main/inc/lib/moodleexport/FileExport.php | 13 +- main/inc/lib/moodleexport/FolderExport.php | 4 +- main/inc/lib/moodleexport/ForumExport.php | 188 ++++++++++++++ main/inc/lib/moodleexport/GlossaryExport.php | 15 +- main/inc/lib/moodleexport/MoodleExport.php | 232 ++++++++++++++++-- main/inc/lib/moodleexport/PageExport.php | 4 + main/inc/lib/moodleexport/QuizExport.php | 2 + main/inc/lib/moodleexport/ResourceExport.php | 27 ++ main/inc/lib/moodleexport/SectionExport.php | 4 + main/inc/lib/moodleexport/UrlExport.php | 5 + .../Component/CourseCopy/CourseSelectForm.php | 7 +- 15 files changed, 658 insertions(+), 159 deletions(-) create mode 100644 main/inc/lib/moodleexport/ForumExport.php diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php index 1030287a0b2..8bb2a9c7b89 100644 --- a/main/coursecopy/export_moodle.php +++ b/main/coursecopy/export_moodle.php @@ -43,57 +43,44 @@ $action = isset($_POST['action']) ? $_POST['action'] : ''; $exportOption = isset($_POST['export_option']) ? $_POST['export_option'] : ''; -if (Security::check_token('post') && - ($action === 'course_select_form' || $exportOption === 'full_export') -) { - // Clear token - Security::clear_token(); - - // Build course object based on the action - if ($action === 'course_select_form') { - $cb = new CourseBuilder('partial'); - $course = $cb->build(0, null, false, array_keys($_POST['resource']), $_POST['resource']); - $course = CourseSelectForm::get_posted_course(null, 0, '', $course); - } else { - $cb = new CourseBuilder('complete'); - $course = $cb->build(); - } +// 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; - // Instantiate MoodleExport and pass course data to it - $exporter = new MoodleExport($course); - $courseId = api_get_course_id(); // Get course ID - $exportDir = 'moodle_export_' . $courseId; - $coursePath = api_get_course_path(); // Get course path - - try { - $moodleVersion = isset($_POST['moodle_version']) ? $_POST['moodle_version'] : '3'; - - // Pass the course data to the export function - $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 (Security::check_token('post') && $exportOption === 'select_items') { - // Clear token - Security::clear_token(); - - // Build course object for partial export - $cb = new CourseBuilder('partial'); - $course = $cb->build(); - if ($course->has_resources()) { - // Add token to Course select form - $hiddenFields['sec_token'] = Security::get_token(); - CourseSelectForm::display_form($course, $hiddenFields, false, true); + 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 = $_POST['admin_username']; + $adminEmail = $_POST['admin_email']; + + $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']) ? $_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('NoResourcesToExport'), 'warning'); + echo Display::return_message(get_lang('NoResourcesSelected'), 'warning'); } } else { $form = new FormValidator( @@ -108,32 +95,76 @@ '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(); - // When progress bar appears we have to hide the title "Please select an export-option". - $form->updateAttributes( - [ - 'onsubmit' => str_replace( - 'javascript: ', - 'javascript: page_title = getElementById(\'page_title\'); if (page_title) { setTimeout(\'page_title.style.display = \\\'none\\\';\', 2000); } ', - $form->getAttribute('onsubmit') - ), - ] - ); - $values['export_option'] = 'full_export'; - $form->setDefaults($values); - // Add Security token - $token = Security::get_token(); - $form->addElement('hidden', 'sec_token'); - $form->setConstants(['sec_token' => $token]); - echo '
    '; - echo '
    '; - echo '
    '; - $form->display(); - echo '
    '; - echo '
    '; - echo '
    '; + 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/moodleexport/ActivityExport.php b/main/inc/lib/moodleexport/ActivityExport.php index e10ede1477e..11334b74d9b 100644 --- a/main/inc/lib/moodleexport/ActivityExport.php +++ b/main/inc/lib/moodleexport/ActivityExport.php @@ -110,37 +110,32 @@ protected function createGradesXml(array $data, string $directory): void } /** - * Creates the inforef.xml file, referencing the files associated with the activity. + * Creates the inforef.xml file, referencing users and files associated with the activity. */ - protected function createInforefXml(array $files, string $directory): void + protected function createInforefXml(array $references, string $directory): void { + // Start the XML content $xmlContent = '' . PHP_EOL; $xmlContent .= '' . PHP_EOL; - // Handle different module types - if (isset($files['modulename']) && $files['modulename'] === 'glossary') { - // For glossary, include user references + // Add user references if provided + if (isset($references['users']) && is_array($references['users'])) { $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $files['userid'] . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; + foreach ($references['users'] as $userId) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($userId) . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; + } $xmlContent .= ' ' . PHP_EOL; - } else { - // Default handling for other modules (e.g., quiz, documents) + } + + // Add file references if provided + if (isset($references['files']) && is_array($references['files'])) { $xmlContent .= ' ' . PHP_EOL; - if (is_array($files)) { - if (isset($files['modulename']) && in_array($files['modulename'], ['quiz'])) { - $xmlContent .= ' 0' . PHP_EOL; - } else { - if (isset($files['id'])) { - $files = [$files]; - } - foreach ($files as $file) { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $file['id'] . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - } - } + foreach ($references['files'] as $file) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . htmlspecialchars($file['id']) . '' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; } $xmlContent .= ' ' . PHP_EOL; } diff --git a/main/inc/lib/moodleexport/AssignExport.php b/main/inc/lib/moodleexport/AssignExport.php index 01d6b5a5381..57e7b1ac925 100644 --- a/main/inc/lib/moodleexport/AssignExport.php +++ b/main/inc/lib/moodleexport/AssignExport.php @@ -37,6 +37,9 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createInforefXml($assignData, $assignDir); $this->createGradeHistoryXml($assignData, $assignDir); $this->createRolesXml($assignData, $assignDir); + $this->createCommentsXml($assignData, $assignDir); + $this->createCalendarXml($assignData, $assignDir); + $this->createFiltersXml($assignData, $assignDir); } /** @@ -147,37 +150,6 @@ private function createGradingXml(array $data, string $assignDir): void $this->createXmlFile('grading', $xmlContent, $assignDir); } - /** - * Create the inforef.xml file for the assign activity. - * This method can be customized to handle attachments. - */ - protected function createInforefXml(array $files, string $directory): void - { - $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - - if (!empty($files['files'])) { - $xmlContent .= ' ' . PHP_EOL; - foreach ($files['files'] as $file) { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $file['id'] . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - } - $xmlContent .= ' ' . PHP_EOL; - } - - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $files['grade_item_id'] . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; - - $xmlContent .= ''; - - $this->createXmlFile('inforef', $xmlContent, $directory); - } - - /** * Get all the data related to the assign activity. */ @@ -199,6 +171,9 @@ public function getData(int $assignId, int $sectionId): ?array } } + $adminData = MoodleExport::getAdminUserData(); + $adminId = $adminData['id']; + return [ 'id' => (int) $work->params['id'], 'moduleid' => (int) $work->params['id'], @@ -214,6 +189,7 @@ public function getData(int $assignId, int $sectionId): ?array '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 index f2976fc848e..24393250aa2 100644 --- a/main/inc/lib/moodleexport/CourseExport.php +++ b/main/inc/lib/moodleexport/CourseExport.php @@ -69,15 +69,53 @@ private function createCourseXml(string $destinationDir): void $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); @@ -106,7 +144,7 @@ private function createEnrolmentsXml(array $enrolmentsData, string $destinationD } /** - * Creates the inforef.xml file with file references and question categories. + * Creates the inforef.xml file with file references, question categories, and role references. */ private function createInforefXml(string $destinationDir): void { @@ -127,13 +165,23 @@ private function createInforefXml(string $destinationDir): void } } - $xmlContent .= ' ' . PHP_EOL; - foreach ($questionCategories as $categoryId) { - $xmlContent .= ' ' . PHP_EOL; - $xmlContent .= ' ' . $categoryId . '' . PHP_EOL; - $xmlContent .= ' ' . PHP_EOL; + if (!empty($questionCategories)) { + $xmlContent .= ' ' . PHP_EOL; + foreach ($questionCategories as $categoryId) { + $xmlContent .= ' ' . PHP_EOL; + $xmlContent .= ' ' . $categoryId . '' . PHP_EOL; + $xmlContent .= ' ' . 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); diff --git a/main/inc/lib/moodleexport/FileExport.php b/main/inc/lib/moodleexport/FileExport.php index 42f6c7f386a..dba80155c90 100644 --- a/main/inc/lib/moodleexport/FileExport.php +++ b/main/inc/lib/moodleexport/FileExport.php @@ -137,6 +137,9 @@ private function createFileXmlEntry(array $file): string */ public function getFilesData(): array { + $adminData = MoodleExport::getAdminUserData(); + $adminId = $adminData['id']; + $filesData = ['files' => []]; foreach ($this->course->resources[RESOURCE_DOCUMENT] as $document) { @@ -160,7 +163,7 @@ public function getFilesData(): array 'filepath' => '/', 'documentpath' => 'document/'.$docData['path'], 'filename' => basename($docData['path']), - 'userid' => api_get_user_id(), + 'userid' => $adminId, 'filesize' => $docData['size'], 'mimetype' => $this->getMimeType($docData['path']), 'status' => 0, @@ -199,6 +202,8 @@ private function processDocument(array $filesData, object $document): array */ private function getFileData(object $document): array { + $adminData = MoodleExport::getAdminUserData(); + $adminId = $adminData['id']; $contenthash = hash('sha1', basename($document->path)); $mimetype = $this->getMimeType($document->path); @@ -212,7 +217,7 @@ private function getFileData(object $document): array 'filepath' => '/', 'documentpath' => $document->path, 'filename' => basename($document->path), - 'userid' => api_get_user_id(), + 'userid' => $adminId, 'filesize' => $document->size, 'mimetype' => $mimetype, 'status' => 0, @@ -229,6 +234,8 @@ private function getFileData(object $document): array */ 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']); @@ -244,7 +251,7 @@ private function getFolderFileData(array $file, int $sourceId): array 'filepath' => $filepath, 'documentpath' => 'document/'.$file['path'], 'filename' => $filename, - 'userid' => api_get_user_id(), + 'userid' => $adminId, 'filesize' => $file['size'], 'mimetype' => $mimetype, 'status' => 0, diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php index c8c2a744c2b..94746e06331 100644 --- a/main/inc/lib/moodleexport/FolderExport.php +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -35,6 +35,8 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createGradeHistoryXml($folderData, $folderDir); $this->createInforefXml($this->getFilesForFolder($activityId), $folderDir); $this->createRolesXml($folderData, $folderDir); + $this->createCommentsXml($folderData, $folderDir); + $this->createCalendarXml($folderData, $folderDir); } /** @@ -99,7 +101,7 @@ private function getFilesForFolder(int $folderId): array } } - return $files; + return ['files' => $files]; } /** 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 index f815663b79b..577b2b6740a 100644 --- a/main/inc/lib/moodleexport/GlossaryExport.php +++ b/main/inc/lib/moodleexport/GlossaryExport.php @@ -80,7 +80,7 @@ private function createGlossaryXml(array $glossaryData, string $glossaryDir): vo $xmlContent .= ' ' . PHP_EOL; $xmlContent .= ' ' . $entry['userid'] . '' . PHP_EOL; $xmlContent .= ' ' . htmlspecialchars($entry['concept']) . '' . PHP_EOL; - $xmlContent .= ' ' . $entry['definition'] . '>' . PHP_EOL; + $xmlContent .= ' ' . PHP_EOL; $xmlContent .= ' 1' . PHP_EOL; $xmlContent .= ' 0' . PHP_EOL; $xmlContent .= ' ' . PHP_EOL; @@ -110,13 +110,16 @@ private function createGlossaryXml(array $glossaryData, string $glossaryDir): vo */ 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' => api_get_user_id(), - 'concept' => 'Concept', - 'definition' => 'Definition', + 'userid' => $adminId, + 'concept' => $glossary->name, + 'definition' => $glossary->description, 'timecreated' => time(), 'timemodified' => time(), ]; @@ -134,8 +137,10 @@ public function getData(int $glossaryId, int $sectionId): ?array 'timemodified' => time(), 'sectionid' => $sectionId, 'sectionnumber' => 0, - 'userid' => api_get_user_id(), + 'userid' => $adminId, 'entries' => $glossaryEntries, + 'users' => [$adminId], + 'files' => [], ]; } } diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index 64dc27b746d..5e26eecf79a 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -19,6 +19,7 @@ class MoodleExport { private $course; + private static $adminUserData = []; /** * Constructor to initialize the course object. @@ -121,8 +122,6 @@ private function createMoodleBackupXml(string $destinationDir, string $version): $xmlContent .= '' . PHP_EOL; $xmlContent .= ' ' . PHP_EOL; - $wwwRoot = api_get_path(WEB_PATH); - $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; @@ -132,16 +131,16 @@ private function createMoodleBackupXml(string $destinationDir, string $version): $xmlContent .= ' 0' . PHP_EOL; $xmlContent .= ' 1' . PHP_EOL; $xmlContent .= ' 0' . PHP_EOL; - $xmlContent .= ' '.$wwwRoot.'' . PHP_EOL; + $xmlContent .= ' ' . $wwwRoot . '' . PHP_EOL; $xmlContent .= ' ' . $siteHash . '' . PHP_EOL; $xmlContent .= ' ' . htmlspecialchars($courseInfo['real_id']) . '' . PHP_EOL; - $xmlContent .= ' '.get_lang('Topics').'' . 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 .= ' ' . $courseInfo['real_id'] . '' . PHP_EOL; + $xmlContent .= ' ' . api_get_current_access_url_id() . '' . PHP_EOL; $xmlContent .= '
    ' . PHP_EOL; $xmlContent .= ' ' . PHP_EOL; @@ -197,12 +196,18 @@ private function createMoodleBackupXml(string $destinationDir, string $version): // Backup settings $xmlContent .= ' ' . PHP_EOL; - $settings = $this->exportBackupSettings(); // Export backup settings dynamically + $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; @@ -211,7 +216,7 @@ private function createMoodleBackupXml(string $destinationDir, string $version): $xmlContent .= ''; $xmlFile = $destinationDir . '/moodle_backup.xml'; - file_put_contents($xmlFile, $xmlContent) !== false; + file_put_contents($xmlFile, $xmlContent); } /** @@ -280,6 +285,13 @@ private function getActivities(): array $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); @@ -503,8 +515,17 @@ public function exportQuestionsXml(array $questionsData, string $exportDir): voi private function exportRolesXml(string $exportDir): void { $xmlContent = '' . PHP_EOL; - $xmlContent .= '' . PHP_EOL; - $xmlContent .= ''; + $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); } @@ -520,26 +541,207 @@ private function exportScalesXml(string $exportDir): void } /** - * Export users data to XML file. + * 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' => 'Lima', + 'country' => 'PE', + '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. + * Export the backup settings, including dynamic settings for sections and activities. */ - private function exportBackupSettings(): array + private function exportBackupSettings(array $sections, array $activities): array { - return [ + // root-level settings + $settings = [ + ['level' => 'root', 'name' => 'filename', 'value' => 'backup-moodle2-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'], - // Add more settings as needed + ['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 index a13ee108e91..e91b46d8fee 100644 --- a/main/inc/lib/moodleexport/PageExport.php +++ b/main/inc/lib/moodleexport/PageExport.php @@ -35,6 +35,8 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createGradeHistoryXml($pageData, $pageDir); $this->createInforefXml($pageData, $pageDir); $this->createRolesXml($pageData, $pageDir); + $this->createCommentsXml($pageData, $pageDir); + $this->createCalendarXml($pageData, $pageDir); } /** @@ -84,6 +86,8 @@ public function getData(int $pageId, int $sectionId): ?array 'sectionnumber' => 1, 'display' => 0, 'timemodified' => time(), + 'users' => [], + 'files' => [], ]; } } diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php index f05d6848c4b..24bac488d92 100644 --- a/main/inc/lib/moodleexport/QuizExport.php +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -101,6 +101,8 @@ public function getData(int $quizId, int $sectionId): array 'completionpass' => $quiz->obj->completionpass ?? 0, 'completionminattempts' => $quiz->obj->completionminattempts ?? 0, 'allowofflineattempts' => $quiz->obj->allowofflineattempts ?? 0, + 'users' => [], + 'files' => [], ]; } } diff --git a/main/inc/lib/moodleexport/ResourceExport.php b/main/inc/lib/moodleexport/ResourceExport.php index 6509cf8fa37..e1e810183e4 100644 --- a/main/inc/lib/moodleexport/ResourceExport.php +++ b/main/inc/lib/moodleexport/ResourceExport.php @@ -35,6 +35,8 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createGradeHistoryXml($resourceData, $resourceDir); $this->createInforefXml($resourceData, $resourceDir); $this->createRolesXml($resourceData, $resourceDir); + $this->createCommentsXml($resourceData, $resourceDir); + $this->createCalendarXml($resourceData, $resourceDir); } /** @@ -62,6 +64,29 @@ private function createResourceXml(array $resourceData, string $resourceDir): vo $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. */ @@ -79,6 +104,8 @@ public function getData(int $resourceId, int $sectionId): array 'sectionid' => $sectionId, 'sectionnumber' => 1, 'timemodified' => time(), + 'users' => [], + 'files' => [], ]; } } diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index e1f19ad5cd0..f6d99e09d5a 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -71,6 +71,7 @@ private function exportActivities(array $activities, string $exportDir, int $sec 'glossary' => GlossaryExport::class, 'url' => UrlExport::class, 'assign' => AssignExport::class, + 'forum' => ForumExport::class, 'page' => PageExport::class, 'resource' => ResourceExport::class, 'folder' => FolderExport::class, @@ -101,6 +102,7 @@ public function getGeneralItems(): array RESOURCE_GLOSSARY => 'glossary_id', RESOURCE_LINK => 'source_id', RESOURCE_WORK => 'source_id', + RESOURCE_FORUM => 'source_id', ]; foreach ($resourceTypes as $resourceType => $idKey) { @@ -214,6 +216,7 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti 'glossary' => GlossaryExport::class, 'url' => UrlExport::class, 'assign' => AssignExport::class, + 'forum' => ForumExport::class, 'page' => PageExport::class, 'resource' => ResourceExport::class, 'folder' => FolderExport::class, @@ -226,6 +229,7 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti case 'glossary': case 'assign': case 'url': + case 'forum': $activityId = $itemType === 'glossary' ? 1 : (int) $item['path']; $exportClass = $activityClassMap[$itemType]; $exportInstance = new $exportClass($this->course); diff --git a/main/inc/lib/moodleexport/UrlExport.php b/main/inc/lib/moodleexport/UrlExport.php index 8f281b3425e..96c728c74f9 100644 --- a/main/inc/lib/moodleexport/UrlExport.php +++ b/main/inc/lib/moodleexport/UrlExport.php @@ -34,6 +34,9 @@ public function export($activityId, $exportDir, $moduleId, $sectionId): void $this->createGradeHistoryXml($urlData, $urlDir); $this->createInforefXml($urlData, $urlDir); $this->createRolesXml($urlData, $urlDir); + $this->createCommentsXml($urlData, $urlDir); + $this->createCalendarXml($urlData, $urlDir); + $this->createFiltersXml($urlData, $urlDir); } /** @@ -79,6 +82,8 @@ public function getData(int $activityId, int $sectionId): ?array '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 ) { From 1738e79231aca1b9bc26ef1bde4cc7fb5c1bb249 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Sun, 15 Sep 2024 10:57:55 -0500 Subject: [PATCH 5/8] Update feedback export handling and question formatting --- main/inc/lib/moodleexport/FeedbackExport.php | 193 ++++++++++++++++++ main/inc/lib/moodleexport/MoodleExport.php | 7 + main/inc/lib/moodleexport/SectionExport.php | 6 +- .../CourseCopy/Resources/SurveyQuestion.php | 3 + 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 main/inc/lib/moodleexport/FeedbackExport.php 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/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index 5e26eecf79a..92b9ef5a922 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -319,6 +319,13 @@ private function getActivities(): array $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) { diff --git a/main/inc/lib/moodleexport/SectionExport.php b/main/inc/lib/moodleexport/SectionExport.php index f6d99e09d5a..77b8624c993 100644 --- a/main/inc/lib/moodleexport/SectionExport.php +++ b/main/inc/lib/moodleexport/SectionExport.php @@ -75,6 +75,7 @@ private function exportActivities(array $activities, string $exportDir, int $sec 'page' => PageExport::class, 'resource' => ResourceExport::class, 'folder' => FolderExport::class, + 'feedback' => FeedbackExport::class, ]; foreach ($activities as $activity) { @@ -103,6 +104,7 @@ public function getGeneralItems(): array RESOURCE_LINK => 'source_id', RESOURCE_WORK => 'source_id', RESOURCE_FORUM => 'source_id', + RESOURCE_SURVEY => 'source_id', ]; foreach ($resourceTypes as $resourceType => $idKey) { @@ -220,9 +222,10 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti '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']); + $itemType = $item['item_type'] === 'link' ? 'url' : ($item['item_type'] === 'work' ? 'assign' : ($item['item_type'] === 'survey' ? 'feedback' : $item['item_type'])); switch ($itemType) { case 'quiz': @@ -230,6 +233,7 @@ private function addActivityToList(array $item, int $sectionId, array &$activiti case 'assign': case 'url': case 'forum': + case 'feedback': $activityId = $itemType === 'glossary' ? 1 : (int) $item['path']; $exportClass = $activityClassMap[$itemType]; $exportInstance = new $exportClass($this->course); 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; From 2bf89b4c487c9c776889c1ad5fa7e76f5306bc19 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 17 Sep 2024 11:10:04 -0500 Subject: [PATCH 6/8] Improve security validation for admin user data --- main/coursecopy/export_moodle.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php index 8bb2a9c7b89..05bde930fa9 100644 --- a/main/coursecopy/export_moodle.php +++ b/main/coursecopy/export_moodle.php @@ -55,8 +55,20 @@ // Get admin details $adminId = (int) $_POST['admin_id']; - $adminUsername = $_POST['admin_username']; - $adminEmail = $_POST['admin_email']; + $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(); + } + + $adminUsername = Security::remove_XSS($adminUsername); + $adminEmail = Security::remove_XSS($adminEmail); $exporter = new MoodleExport($course); $exporter->setAdminUserData($adminId, $adminUsername, $adminEmail); From 811dfa6bac5f6deee036c5e649cfcd29edfe99ab Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 17 Sep 2024 11:22:14 -0500 Subject: [PATCH 7/8] Review feedback and improvements --- main/coursecopy/export_moodle.php | 2 +- main/inc/lib/moodleexport/FolderExport.php | 2 +- main/inc/lib/moodleexport/MoodleExport.php | 20 ++++++++++---------- main/inc/lib/moodleexport/QuizExport.php | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php index 05bde930fa9..c5432755dd6 100644 --- a/main/coursecopy/export_moodle.php +++ b/main/coursecopy/export_moodle.php @@ -77,7 +77,7 @@ $courseId = api_get_course_id(); $exportDir = 'moodle_export_' . $courseId; try { - $moodleVersion = isset($_POST['moodle_version']) ? $_POST['moodle_version'] : '3'; + $moodleVersion = isset($_POST['moodle_version']) ? (int) $_POST['moodle_version'] : 3; $mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion); echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm'); diff --git a/main/inc/lib/moodleexport/FolderExport.php b/main/inc/lib/moodleexport/FolderExport.php index 94746e06331..e8a0814058c 100644 --- a/main/inc/lib/moodleexport/FolderExport.php +++ b/main/inc/lib/moodleexport/FolderExport.php @@ -95,7 +95,7 @@ private function getFilesForFolder(int $folderId): array 'contenthash' => 'hash' . $doc['id'], 'filename' => $doc['basename'], 'filepath' => $doc['path'], - 'filesize' => (int)$doc['size'], + 'filesize' => (int) $doc['size'], 'mimetype' => $this->getMimeType($doc['basename']), ]; } diff --git a/main/inc/lib/moodleexport/MoodleExport.php b/main/inc/lib/moodleexport/MoodleExport.php index 92b9ef5a922..825f5595125 100644 --- a/main/inc/lib/moodleexport/MoodleExport.php +++ b/main/inc/lib/moodleexport/MoodleExport.php @@ -32,7 +32,7 @@ public function __construct(object $course) /** * Export the Moodle course in .mbz format. */ - public function export(string $courseId, string $exportDir, string $version) + public function export(string $courseId, string $exportDir, int $version) { $tempDir = api_get_path(SYS_ARCHIVE_PATH) . $exportDir; @@ -109,7 +109,7 @@ private function exportRootXmlFiles(string $exportDir): void /** * Create the moodle_backup.xml file with the required course details. */ - private function createMoodleBackupXml(string $destinationDir, string $version): void + private function createMoodleBackupXml(string $destinationDir, int $version): void { // Generate course information and backup metadata $courseInfo = api_get_course_info($this->course->code); @@ -123,10 +123,10 @@ private function createMoodleBackupXml(string $destinationDir, string $version): $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 .= ' ' . ($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; @@ -498,7 +498,7 @@ public function exportQuestionsXml(array $questionsData, string $exportDir): voi $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 .= ' my.moodle3.com+' . time() . '+CATEGORYSTAMP' . PHP_EOL; + $xmlContent .= ' moodle+' . time() . '+CATEGORYSTAMP' . PHP_EOL; $xmlContent .= ' 0' . PHP_EOL; $xmlContent .= ' 999' . PHP_EOL; $xmlContent .= ' $@NULL@$' . PHP_EOL; @@ -563,8 +563,8 @@ public function setAdminUserData(int $id, string $username, string $email): void 'institution' => '', 'department' => '', 'address' => '', - 'city' => 'Lima', - 'country' => 'PE', + 'city' => 'London', + 'country' => 'GB', 'lastip' => '127.0.0.1', 'picture' => '0', 'description' => '', @@ -694,7 +694,7 @@ private function exportBackupSettings(array $sections, array $activities): array { // root-level settings $settings = [ - ['level' => 'root', 'name' => 'filename', 'value' => 'backup-moodle2-course-' . time() . '.mbz'], + ['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'], diff --git a/main/inc/lib/moodleexport/QuizExport.php b/main/inc/lib/moodleexport/QuizExport.php index 24bac488d92..a2261945936 100644 --- a/main/inc/lib/moodleexport/QuizExport.php +++ b/main/inc/lib/moodleexport/QuizExport.php @@ -321,8 +321,8 @@ public function exportQuestion(array $question): string $xmlContent .= ' 0.3333333' . PHP_EOL; $xmlContent .= ' ' . htmlspecialchars(str_replace('_nosingle', '', $question['qtype']) ?? 'unknown') . '' . PHP_EOL; $xmlContent .= ' 1' . PHP_EOL; - $xmlContent .= ' my.moodle3.com+' . time() . '+QUESTIONSTAMP' . PHP_EOL; - $xmlContent .= ' my.moodle3.com+' . time() . '+VERSIONSTAMP' . 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; From deb00221a54949410b058ffe645004450069090d Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Tue, 17 Sep 2024 23:51:26 +0200 Subject: [PATCH 8/8] Maintenance: Moodle export: Remove calls to remove_XSS() as unnecessary (security checks before that are now enough) - refs BT#21977 --- main/coursecopy/export_moodle.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/main/coursecopy/export_moodle.php b/main/coursecopy/export_moodle.php index c5432755dd6..b061bf9f219 100644 --- a/main/coursecopy/export_moodle.php +++ b/main/coursecopy/export_moodle.php @@ -67,9 +67,6 @@ exit(); } - $adminUsername = Security::remove_XSS($adminUsername); - $adminEmail = Security::remove_XSS($adminEmail); - $exporter = new MoodleExport($course); $exporter->setAdminUserData($adminId, $adminUsername, $adminEmail);