MDL-69156 backup: correct behaviour of course copy idnumber field.
[moodle.git] / backup / util / ui / classes / copy / copy.php
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/>.
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  */
29 namespace core_backup\copy;
31 defined('MOODLE_INTERNAL') || die;
33 global $CFG;
34 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
35 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
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  */
48 class copy  {
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     );
67     /**
68      * Data required for course copy operations.
69      *
70      * @var array
71      */
72     private $copydata = array();
74     /**
75      * List of role ids to keep enrolments for in the destination course.
76      *
77      * @var array
78      */
79     private $roles = array();
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     }
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();
101         foreach ($formdata as $key => $value) {
102             if ((substr($key, 0, 5 ) === 'role_') && ($value != 0)) {
103                 $keptroles[] = $value;
104             }
105         }
107         return $keptroles;
108     }
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 final function get_copy_data(\stdClass $formdata): \stdClass {
118         $copydata = new \stdClass();
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         }
128         return $copydata;
129     }
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();
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();
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();
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();
163         $rc->set_copy($copydata);
164         $rc->save_controller();
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);
172         // Clean up the controller.
173         $bc->destroy();
175         return $copyids;
176     }
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();
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     }
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';
227         $copyrecords = $DB->get_records_sql($sql, $params);
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;
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);
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();
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);
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());
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             }
299             $copies[] = $copy;
300         }
302         // Extra processing to filter records for a given course.
303         if ($courseid != 0 ) {
304             $copies = self::filter_copies_course($copies, $courseid);
305         }
307         return $copies;
308     }