moodle-local_replication/import_cli.php
Kumi f9ed77bcde
feat(replication): enhance course and category management
Implement enhanced category and course replication, improving the handling of both by ensuring new and existing categories are processed correctly. Introduce CLI support for bulk category imports using a dedicated flag and provide helper functions like `generateRandomString` and `removePath` for file management. Add a new `trigger_all.php` script to trigger replication for all courses and an `unenroll.php` script for unenrolling users with cleanup of related data.

Includes error handling improvements and directory structure checks.
2024-09-09 14:55:36 +02:00

327 lines
9.9 KiB
PHP

<?php
define('CLI_SCRIPT', true);
require_once("../../config.php");
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . "/backup/util/includes/restore_includes.php");
require_once($CFG->libdir . '/clilib.php');
function generateRandomString($length = 10) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
function removePath($path) {
if(is_dir($path)) {
$handler = opendir($path);
if (!$handler) {
trigger_error('File Error: Failed to open the directory ' . $path, E_USER_ERROR);
return;
}
// list the files in the directory
while ($file = readdir($handler)) {
if ($file != '.' && $file != '..')
removePath($path.DIRECTORY_SEPARATOR.$file);
}
// tidy up: close the handler
closedir($handler);
if (!rmdir($path)) {
#trigger_error('File Error: Failed to remove folder ' . $path, E_USER_ERROR);
}
}
else {
// delete it
if (!unlink($path)) {
#trigger_error('File Error: Failed to remove file ' . $path, E_USER_ERROR);
}
}
}
function process_category($category, $parent_id = 0) {
global $DB;
$oldid = (int)$category['oldid'];
$name = (string)$category['name'];
$existing_category = \core_course_category::get($oldid, IGNORE_MISSING);
if ($existing_category->id) {
// Update name and parent if necessary
$updated_data = [];
if ($existing_category->name !== $name) {
$updated_data['name'] = $name;
}
if ($parent_id !== 0 && $existing_category->parent != $parent_id) {
$updated_data['parent'] = $parent_id;
}
if (!empty($updated_data)) {
// Update category
$existing_category->update($updated_data);
}
} else {
// Create categories up to the required ID
$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {course_categories}");
while ($max_id < $oldid) {
$new_category_data = [
'name' => "Placeholder " . ($max_id + 1),
'parent' => $parent_id,
'idnumber' => '',
'visible' => 1
];
// Create placeholder category
\core_course_category::create($new_category_data);
$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {course_categories}");
}
// Create target category
$new_category_data = [
'id' => $oldid,
'name' => $name,
'parent' => $parent_id,
'idnumber' => '',
'visible' => 1
];
$existing_category = \core_course_category::get($oldid, MUST_EXIST, true);
$existing_category->update($new_category_data);
}
// Process sub-categories
foreach ($category->category as $sub_category) {
process_category($sub_category, $oldid);
}
}
$usage = "Import a course from ContentMonster
Usage:
# php import_cli.php --courseid=<value> --categoryid=<value> --timestamp=<value> [--source=<value>] [--categories]
# php import_cli.php [--help|-h]
Options:
-h --help Print this help.
--paramname=<value> Describe the parameter and the meaning of its values.
";
list($options, $unrecognised) = cli_get_params([
'help' => false,
'categories' => false,
'courseid' => null,
'categoryid' => null,
'timestamp' => null,
'source' => null,
], [
'h' => 'help',
'c' => 'categories'
]);
if ($unrecognised) {
$unrecognised = implode(PHP_EOL . ' ', $unrecognised);
cli_error(get_string('cliunknowoption', 'core_admin', $unrecognised));
}
if ($options['help']) {
cli_writeln($usage);
exit(2);
}
$replicationconfig = get_config('local_replication');
$directory = $replicationconfig->directory;
if (empty($options['timestamp'])) {
cli_error('Missing mandatory argument timestamp.', 2);
}
$timestamp = $options["timestamp"];
if (!empty($options['categories'])) {
$path = $directory . DIRECTORY_SEPARATOR . "categories_$timestamp.xml";
$xml = simplexml_load_file($path);
foreach ($xml->category as $category) {
process_category($category);
}
removePath($path);
echo("OK");
exit();
}
if (empty($options['courseid'])) {
cli_error('Missing mandatory argument courseid.', 2);
}
if (empty($options['categoryid'])) {
cli_error('Missing mandatory argument categoryid.', 2);
}
$courseid = $options['courseid'];
$categoryid = $options["categoryid"];
try {
$course = get_course($courseid);
} catch (Exception $ex) {
$course = null;
}
try {
$category = $DB->get_record('course_categories', array('id' => $categoryid), '*', MUST_EXIST);
} catch (Exception $ex) {
cli_error('Category " . $categoryid . " does not exist.');
}
if (!$course) {
while (!$DB->get_record('course', array('id' => ((int) $courseid) - 1), '*')) {
$name = generateRandomString();
$newcourse = restore_dbops::create_new_course("Placeholder " . $name, $name, $category->id);
if ((int) $newcourse > (int) $courseid) {
cli_error('Did you delete courses by any chance? Cannot create course ' . $courseid . '.');
}
}
}
$infile = $directory . DIRECTORY_SEPARATOR . "course_" . $courseid . "_" . $categoryid . "_" . $timestamp . ".mbz";
if (empty($CFG->tempdir)) {
$CFG->tempdir = $CFG->dataroot . DIRECTORY_SEPARATOR . 'temp';
}
if (!file_exists($infile)) {
cli_error("Backup file '" . $infile . "' does not exist.");
}
if (!is_readable($infile)) {
cli_error("Backup file '" . $infile . "' is not readable.");
}
$backupdir = "restore_" . uniqid();
if (isset($CFG->backuptempdir)){
$path = $CFG->backuptempdir . DIRECTORY_SEPARATOR . $backupdir;
}
else{
$path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir;
}
$fp = get_file_packer('application/vnd.moodle.backup');
$fp->extract_to_pathname($infile, $path);
$xmlfile = $path . DIRECTORY_SEPARATOR . "course" . DIRECTORY_SEPARATOR . "course.xml";
$xml = simplexml_load_file($xmlfile);
$fullname = $xml->xpath('/course/fullname');
if (!$fullname) {
$fullname = $xml->xpath('/MOODLE_BACKUP/COURSE/HEADER/FULLNAME');
}
$shortname = $xml->xpath('/course/shortname');
if (!$shortname) {
$shortname = $xml->xpath('/MOODLE_BACKUP/COURSE/HEADER/SHORTNAME');
}
$fullname = (string)($fullname[0]);
$shortname = (string)($shortname[0]);
if (!$fullname) {
$fullname = $shortname;
}
$questionfile = $path . DIRECTORY_SEPARATOR . "questions.xml";
if (file_exists($questionfile)) {
$questions = new DOMDocument();
$questions->load($questionfile);
$questions->save($questionfile . ".bak");
foreach ($questions->getElementsByTagName("question") as $question) {
$questiontext = $question->getElementsByTagName("questiontext")->item(0);
$newquestiontext = strip_tags(html_entity_decode($questiontext->textContent));
$questiontext->removeChild($questiontext->firstChild);
$questiontext->appendChild(new DOMText($newquestiontext));
foreach ($question->getElementsByTagName("answer") as $answer) {
$answertext = $answer->getElementsByTagName("answertext")->item(0);
$newanswertext = strip_tags(html_entity_decode($answertext->textContent));
$answertext->removeChild($answertext->firstChild);
$answertext->appendChild(new DOMText($newanswertext));
}
}
$questions->save($questionfile);
}
if (!$course && $DB->get_record('course', array('category' => $category->id, 'shortname' => $shortname))) {
$matches = NULL;
preg_match('/(.*)_(\d+)$/', $shortname, $matches);
if ($matches) {
$base = $matches[1];
$number = $matches[2];
} else {
$base = $shortname;
$number = 1;
}
$shortname = $base . '_' . $number;
while ($DB->get_record('course', array('category' => $category->id, 'shortname' => $shortname))) {
$number++;
$shortname = $base . '_' . $number;
}
}
if ($course) {
cli_writeln("Overwriting current content of existing course -> Course ID: $courseid");
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, 2, 0);
$rc->get_plan()->get_setting('overwrite_conf')->set_value(true);
$rc->get_plan()->get_setting('keep_roles_and_enrolments')->set_value(true);
$rc->get_plan()->get_setting('keep_groups_and_groupings')->set_value(true);
} else {
cli_writeln("Creating new course to restore backup");
$courseid = restore_dbops::create_new_course($fullname, $shortname, $category->id);
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, 2, backup::TARGET_NEW_COURSE);
}
if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
$rc->convert();
}
$plan = $rc->get_plan();
if (!$rc->execute_precheck()){
$check = $rc->get_precheck_results();
cli_error("Restore pre-check failed!");
}
$deletingoptions = array();
$deletingoptions['keep_roles_and_enrolments'] = 1;
$deletingoptions['keep_groups_and_groupings'] = 1;
restore_dbops::delete_course_content($courseid, $deletingoptions);
$rc->execute_plan();
$rc->destroy();
$course = get_course($courseid);
$course->category = $categoryid;
$course->fullname = $fullname;
$course->shortname = $shortname;
$DB->update_record('course', $course);
try {
removePath($path);
} catch (\Throwable $th) {
cli_writeln("Could not remove temporary files. Please remove them manually.");
}
cli_writeln("New course ID for '$shortname': $courseid in category {$category->id}");
cli_writeln("OK");