Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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  
  29  namespace core_backup\copy;
  30  
  31  defined('MOODLE_INTERNAL') || die;
  32  
  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');
  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   */
  48  class 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.
 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();
 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  }