MDL-69156 backup: correct behaviour of course copy idnumber field.
[moodle.git] / backup / util / ui / classes / copy / copy.php
CommitLineData
01436f75
MP
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 final 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.
01436f75 114 * @return \stdClass $copydata Data required for course copy operations.
f2b12423 115 * @throws \moodle_exception If one of the required copy fields is missing
01436f75
MP
116 */
117 private final 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 {
f2b12423 124 throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, $field);
01436f75
MP
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}