1<?php 2// This file is part of Moodle - http://moodle.org/ 3// 4// Moodle is free software: you can redistribute it and/or modify 5// it under the terms of the GNU General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// Moodle is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU General Public License for more details. 13// 14// You should have received a copy of the GNU General Public License 15// along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17/** 18 * Course copy class. 19 * 20 * Handles procesing data submitted by UI copy form 21 * and sets up the course copy process. 22 * 23 * @package core_backup 24 * @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/> 25 * @author Matt Porritt <mattp@catalyst-au.net> 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 */ 28 29namespace core_backup\copy; 30 31defined('MOODLE_INTERNAL') || die; 32 33global $CFG; 34require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); 35require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 36 37/** 38 * Course copy class. 39 * 40 * Handles procesing data submitted by UI copy form 41 * and sets up the course copy process. 42 * 43 * @package core_backup 44 * @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/> 45 * @author Matt Porritt <mattp@catalyst-au.net> 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48class copy { 49 50 /** 51 * The fields required for copy operations. 52 * 53 * @var array 54 */ 55 private $copyfields = array( 56 'courseid', // Course id integer. 57 'fullname', // Fullname of the destination course. 58 'shortname', // Shortname of the destination course. 59 'category', // Category integer ID that contains the destination course. 60 'visible', // Integer to detrmine of the copied course will be visible. 61 'startdate', // Integer timestamp of the start of the destination course. 62 'enddate', // Integer timestamp of the end of the destination course. 63 'idnumber', // ID of the destination course. 64 'userdata', // Integer to determine if the copied course will contain user data. 65 ); 66 67 /** 68 * Data required for course copy operations. 69 * 70 * @var array 71 */ 72 private $copydata = array(); 73 74 /** 75 * List of role ids to keep enrolments for in the destination course. 76 * 77 * @var array 78 */ 79 private $roles = array(); 80 81 /** 82 * Constructor for the class. 83 * 84 * @param \stdClass $formdata Data from the validated course copy form. 85 */ 86 public function __construct(\stdClass $formdata) { 87 $this->copydata = $this->get_copy_data($formdata); 88 $this->roles = $this->get_enrollment_roles($formdata); 89 } 90 91 /** 92 * Extract the enrolment roles to keep in the copied course 93 * from the raw submitted form data. 94 * 95 * @param \stdClass $formdata Data from the validated course copy form. 96 * @return array $keptroles The roles to keep. 97 */ 98 private function get_enrollment_roles(\stdClass $formdata): array { 99 $keptroles = array(); 100 101 foreach ($formdata as $key => $value) { 102 if ((substr($key, 0, 5 ) === 'role_') && ($value != 0)) { 103 $keptroles[] = $value; 104 } 105 } 106 107 return $keptroles; 108 } 109 110 /** 111 * Take the validated form data and extract the required information for copy operations. 112 * 113 * @param \stdClass $formdata Data from the validated course copy form. 114 * @return \stdClass $copydata Data required for course copy operations. 115 * @throws \moodle_exception If one of the required copy fields is missing 116 */ 117 private function get_copy_data(\stdClass $formdata): \stdClass { 118 $copydata = new \stdClass(); 119 120 foreach ($this->copyfields as $field) { 121 if (isset($formdata->{$field})) { 122 $copydata->{$field} = $formdata->{$field}; 123 } else { 124 throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, $field); 125 } 126 } 127 128 return $copydata; 129 } 130 131 /** 132 * Creates a course copy. 133 * Sets up relevant controllers and adhoc task. 134 * 135 * @return array $copyids THe backup and restore controller ids. 136 */ 137 public function create_copy(): array { 138 global $USER; 139 $copyids = array(); 140 141 // Create the initial backupcontoller. 142 $bc = new \backup_controller(\backup::TYPE_1COURSE, $this->copydata->courseid, \backup::FORMAT_MOODLE, 143 \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); 144 $copyids['backupid'] = $bc->get_backupid(); 145 146 // Create the initial restore contoller. 147 list($fullname, $shortname) = \restore_dbops::calculate_course_names( 148 0, get_string('copyingcourse', 'backup'), get_string('copyingcourseshortname', 'backup')); 149 $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $this->copydata->category); 150 $rc = new \restore_controller($copyids['backupid'], $newcourseid, 151 \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, 152 \backup::TARGET_NEW_COURSE); 153 $copyids['restoreid'] = $rc->get_restoreid(); 154 155 // Configure the controllers based on the submitted data. 156 $copydata = $this->copydata; 157 $copydata->copyids = $copyids; 158 $copydata->keptroles = $this->roles; 159 $bc->set_copy($copydata); 160 $bc->set_status(\backup::STATUS_AWAITING); 161 $bc->get_status(); 162 163 $rc->set_copy($copydata); 164 $rc->save_controller(); 165 166 // Create the ad-hoc task to perform the course copy. 167 $asynctask = new \core\task\asynchronous_copy_task(); 168 $asynctask->set_blocking(false); 169 $asynctask->set_custom_data($copyids); 170 \core\task\manager::queue_adhoc_task($asynctask); 171 172 // Clean up the controller. 173 $bc->destroy(); 174 175 return $copyids; 176 } 177 178 /** 179 * Filters an array of copy records by course ID. 180 * 181 * @param array $copyrecords 182 * @param int $courseid 183 * @return array $copies Filtered array of records. 184 */ 185 static private function filter_copies_course(array $copyrecords, int $courseid): array { 186 $copies = array(); 187 188 foreach ($copyrecords as $copyrecord) { 189 if ($copyrecord->operation == \backup::OPERATION_RESTORE) { // Restore records. 190 if ($copyrecord->status == \backup::STATUS_FINISHED_OK 191 || $copyrecord->status == \backup::STATUS_FINISHED_ERR) { 192 continue; 193 } else { 194 $rc = \restore_controller::load_controller($copyrecord->restoreid); 195 if ($rc->get_copy()->courseid == $courseid) { 196 $copies[] = $copyrecord; 197 } 198 } 199 } else { // Backup records. 200 if ($copyrecord->itemid == $courseid) { 201 $copies[] = $copyrecord; 202 } 203 } 204 } 205 return $copies; 206 } 207 208 /** 209 * Get the in progress course copy operations for a user. 210 * 211 * @param int $userid User id to get the course copies for. 212 * @param int $courseid The optional source course id to get copies for. 213 * @return array $copies Details of the inprogress copies. 214 */ 215 static public function get_copies(int $userid, int $courseid=0): array { 216 global $DB; 217 $copies = array(); 218 $params = array($userid, \backup::EXECUTION_DELAYED, \backup::MODE_COPY); 219 $sql = 'SELECT bc.backupid, bc.itemid, bc.operation, bc.status, bc.timecreated 220 FROM {backup_controllers} bc 221 INNER JOIN {course} c ON bc.itemid = c.id 222 WHERE bc.userid = ? 223 AND bc.execution = ? 224 AND bc.purpose = ? 225 ORDER BY bc.timecreated DESC'; 226 227 $copyrecords = $DB->get_records_sql($sql, $params); 228 229 foreach ($copyrecords as $copyrecord) { 230 $copy = new \stdClass(); 231 $copy->itemid = $copyrecord->itemid; 232 $copy->time = $copyrecord->timecreated; 233 $copy->operation = $copyrecord->operation; 234 $copy->status = $copyrecord->status; 235 $copy->backupid = null; 236 $copy->restoreid = null; 237 238 if ($copyrecord->operation == \backup::OPERATION_RESTORE) { 239 $copy->restoreid = $copyrecord->backupid; 240 // If record is complete or complete with errors, it means the backup also completed. 241 // It also means there are no controllers. In this case just skip and move on. 242 if ($copyrecord->status == \backup::STATUS_FINISHED_OK 243 || $copyrecord->status == \backup::STATUS_FINISHED_ERR) { 244 continue; 245 } else if ($copyrecord->status > \backup::STATUS_REQUIRE_CONV) { 246 // If record is a restore and it's in progress (>200), it means the backup is finished. 247 // In this case return the restore. 248 $rc = \restore_controller::load_controller($copyrecord->backupid); 249 $course = get_course($rc->get_copy()->courseid); 250 251 $copy->source = $course->shortname; 252 $copy->sourceid = $course->id; 253 $copy->destination = $rc->get_copy()->shortname; 254 $copy->backupid = $rc->get_copy()->copyids['backupid']; 255 $rc->destroy(); 256 257 } else if ($copyrecord->status == \backup::STATUS_REQUIRE_CONV) { 258 // If record is a restore and it is waiting (=200), load the controller 259 // and check the status of the backup. 260 // If the backup has finished successfully we have and edge case. Process as per in progress restore. 261 // If the backup has any other code it will be handled by backup processing. 262 $rc = \restore_controller::load_controller($copyrecord->backupid); 263 $bcid = $rc->get_copy()->copyids['backupid']; 264 if (empty($copyrecords[$bcid])) { 265 continue; 266 } 267 $backuprecord = $copyrecords[$bcid]; 268 $backupstatus = $backuprecord->status; 269 if ($backupstatus == \backup::STATUS_FINISHED_OK) { 270 $course = get_course($rc->get_copy()->courseid); 271 272 $copy->source = $course->shortname; 273 $copy->sourceid = $course->id; 274 $copy->destination = $rc->get_copy()->shortname; 275 $copy->backupid = $rc->get_copy()->copyids['backupid']; 276 } else { 277 continue; 278 } 279 } 280 } else { // Record is a backup. 281 $copy->backupid = $copyrecord->backupid; 282 if ($copyrecord->status == \backup::STATUS_FINISHED_OK 283 || $copyrecord->status == \backup::STATUS_FINISHED_ERR) { 284 // If successfully finished then skip it. Restore procesing will look after it. 285 // If it has errored then we can't go any further. 286 continue; 287 } else { 288 // If is in progress then process it. 289 $bc = \backup_controller::load_controller($copyrecord->backupid); 290 $course = get_course($bc->get_courseid()); 291 292 $copy->source = $course->shortname; 293 $copy->sourceid = $course->id; 294 $copy->destination = $bc->get_copy()->shortname; 295 $copy->restoreid = $bc->get_copy()->copyids['restoreid']; 296 } 297 } 298 299 $copies[] = $copy; 300 } 301 302 // Extra processing to filter records for a given course. 303 if ($courseid != 0 ) { 304 $copies = self::filter_copies_course($copies, $courseid); 305 } 306 307 return $copies; 308 } 309} 310