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] [Versions 39 and 310]

   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   * File containing the course class.
  19   *
  20   * @package    tool_uploadcourse
  21   * @copyright  2013 Frédéric Massart
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  27  require_once($CFG->dirroot . '/course/lib.php');
  28  
  29  /**
  30   * Course class.
  31   *
  32   * @package    tool_uploadcourse
  33   * @copyright  2013 Frédéric Massart
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class tool_uploadcourse_course {
  37  
  38      /** Outcome of the process: creating the course */
  39      const DO_CREATE = 1;
  40  
  41      /** Outcome of the process: updating the course */
  42      const DO_UPDATE = 2;
  43  
  44      /** Outcome of the process: deleting the course */
  45      const DO_DELETE = 3;
  46  
  47      /** @var array final import data. */
  48      protected $data = array();
  49  
  50      /** @var array default values. */
  51      protected $defaults = array();
  52  
  53      /** @var array enrolment data. */
  54      protected $enrolmentdata;
  55  
  56      /** @var array errors. */
  57      protected $errors = array();
  58  
  59      /** @var int the ID of the course that had been processed. */
  60      protected $id;
  61  
  62      /** @var array containing options passed from the processor. */
  63      protected $importoptions = array();
  64  
  65      /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */
  66      protected $mode;
  67  
  68      /** @var array course import options. */
  69      protected $options = array();
  70  
  71      /** @var int constant value of self::DO_*, what to do with that course */
  72      protected $do;
  73  
  74      /** @var bool set to true once we have prepared the course */
  75      protected $prepared = false;
  76  
  77      /** @var bool set to true once we have started the process of the course */
  78      protected $processstarted = false;
  79  
  80      /** @var array course import data. */
  81      protected $rawdata = array();
  82  
  83      /** @var array restore directory. */
  84      protected $restoredata;
  85  
  86      /** @var string course shortname. */
  87      protected $shortname;
  88  
  89      /** @var array errors. */
  90      protected $statuses = array();
  91  
  92      /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */
  93      protected $updatemode;
  94  
  95      /** @var array fields allowed as course data. */
  96      static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', 'enddate',
  97          'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes',
  98          'groupmode', 'groupmodeforce', 'enablecompletion');
  99  
 100      /** @var array fields required on course creation. */
 101      static protected $mandatoryfields = array('fullname', 'category');
 102  
 103      /** @var array fields which are considered as options. */
 104      static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null,
 105          'templatecourse' => null, 'reset' => false);
 106  
 107      /** @var array options determining what can or cannot be done at an import level. */
 108      static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false,
 109          'reset' => false, 'restoredir' => null, 'shortnametemplate' => null);
 110  
 111      /**
 112       * Constructor
 113       *
 114       * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_*
 115       * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_*
 116       * @param array $rawdata raw course data.
 117       * @param array $defaults default course data.
 118       * @param array $importoptions import options.
 119       */
 120      public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) {
 121  
 122          if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW &&
 123                  $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL &&
 124                  $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE &&
 125                  $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
 126              throw new coding_exception('Incorrect mode.');
 127          } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING &&
 128                  $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY &&
 129                  $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS &&
 130                  $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) {
 131              throw new coding_exception('Incorrect update mode.');
 132          }
 133  
 134          $this->mode = $mode;
 135          $this->updatemode = $updatemode;
 136  
 137          if (isset($rawdata['shortname'])) {
 138              $this->shortname = $rawdata['shortname'];
 139          }
 140          $this->rawdata = $rawdata;
 141          $this->defaults = $defaults;
 142  
 143          // Extract course options.
 144          foreach (self::$optionfields as $option => $default) {
 145              $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default;
 146          }
 147  
 148          // Import options.
 149          foreach (self::$importoptionsdefaults as $option => $default) {
 150              $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default;
 151          }
 152      }
 153  
 154      /**
 155       * Does the mode allow for course creation?
 156       *
 157       * @return bool
 158       */
 159      public function can_create() {
 160          return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
 161              tool_uploadcourse_processor::MODE_CREATE_NEW,
 162              tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
 163          );
 164      }
 165  
 166      /**
 167       * Does the mode allow for course deletion?
 168       *
 169       * @return bool
 170       */
 171      public function can_delete() {
 172          return $this->importoptions['candelete'];
 173      }
 174  
 175      /**
 176       * Does the mode only allow for course creation?
 177       *
 178       * @return bool
 179       */
 180      public function can_only_create() {
 181          return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
 182              tool_uploadcourse_processor::MODE_CREATE_NEW));
 183      }
 184  
 185      /**
 186       * Does the mode allow for course rename?
 187       *
 188       * @return bool
 189       */
 190      public function can_rename() {
 191          return $this->importoptions['canrename'];
 192      }
 193  
 194      /**
 195       * Does the mode allow for course reset?
 196       *
 197       * @return bool
 198       */
 199      public function can_reset() {
 200          return $this->importoptions['canreset'];
 201      }
 202  
 203      /**
 204       * Does the mode allow for course update?
 205       *
 206       * @return bool
 207       */
 208      public function can_update() {
 209          return in_array($this->mode,
 210                  array(
 211                      tool_uploadcourse_processor::MODE_UPDATE_ONLY,
 212                      tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
 213                  ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING;
 214      }
 215  
 216      /**
 217       * Can we use default values?
 218       *
 219       * @return bool
 220       */
 221      public function can_use_defaults() {
 222          return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS,
 223              tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS));
 224      }
 225  
 226      /**
 227       * Delete the current course.
 228       *
 229       * @return bool
 230       */
 231      protected function delete() {
 232          global $DB;
 233          $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname',
 234              array('shortname' => $this->shortname), MUST_EXIST);
 235          return delete_course($this->id, false);
 236      }
 237  
 238      /**
 239       * Log an error
 240       *
 241       * @param string $code error code.
 242       * @param lang_string $message error message.
 243       * @return void
 244       */
 245      protected function error($code, lang_string $message) {
 246          if (array_key_exists($code, $this->errors)) {
 247              throw new coding_exception('Error code already defined');
 248          }
 249          $this->errors[$code] = $message;
 250      }
 251  
 252      /**
 253       * Return whether the course exists or not.
 254       *
 255       * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty.
 256       * @return bool
 257       */
 258      protected function exists($shortname = null) {
 259          global $DB;
 260          if (is_null($shortname)) {
 261              $shortname = $this->shortname;
 262          }
 263          if (!empty($shortname) || is_numeric($shortname)) {
 264              return $DB->record_exists('course', array('shortname' => $shortname));
 265          }
 266          return false;
 267      }
 268  
 269      /**
 270       * Return the data that will be used upon saving.
 271       *
 272       * @return null|array
 273       */
 274      public function get_data() {
 275          return $this->data;
 276      }
 277  
 278      /**
 279       * Return the errors found during preparation.
 280       *
 281       * @return array
 282       */
 283      public function get_errors() {
 284          return $this->errors;
 285      }
 286  
 287      /**
 288       * Return array of valid fields for default values
 289       *
 290       * @return array
 291       */
 292      protected function get_valid_fields() {
 293          return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names());
 294      }
 295  
 296      /**
 297       * Assemble the course data based on defaults.
 298       *
 299       * This returns the final data to be passed to create_course().
 300       *
 301       * @param array $data current data.
 302       * @return array
 303       */
 304      protected function get_final_create_data($data) {
 305          foreach ($this->get_valid_fields() as $field) {
 306              if (!isset($data[$field]) && isset($this->defaults[$field])) {
 307                  $data[$field] = $this->defaults[$field];
 308              }
 309          }
 310          $data['shortname'] = $this->shortname;
 311          return $data;
 312      }
 313  
 314      /**
 315       * Assemble the course data based on defaults.
 316       *
 317       * This returns the final data to be passed to update_course().
 318       *
 319       * @param array $data current data.
 320       * @param bool $usedefaults are defaults allowed?
 321       * @param bool $missingonly ignore fields which are already set.
 322       * @return array
 323       */
 324      protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) {
 325          global $DB;
 326          $newdata = array();
 327          $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
 328          foreach ($this->get_valid_fields() as $field) {
 329              if ($missingonly) {
 330                  if (isset($existingdata->$field) and $existingdata->$field !== '') {
 331                      continue;
 332                  }
 333              }
 334              if (isset($data[$field])) {
 335                  $newdata[$field] = $data[$field];
 336              } else if ($usedefaults && isset($this->defaults[$field])) {
 337                  $newdata[$field] = $this->defaults[$field];
 338              }
 339          }
 340          $newdata['id'] =  $existingdata->id;
 341          return $newdata;
 342      }
 343  
 344      /**
 345       * Return the ID of the processed course.
 346       *
 347       * @return int|null
 348       */
 349      public function get_id() {
 350          if (!$this->processstarted) {
 351              throw new coding_exception('The course has not been processed yet!');
 352          }
 353          return $this->id;
 354      }
 355  
 356      /**
 357       * Get the directory of the object to restore.
 358       *
 359       * @return string|false|null subdirectory in $CFG->backuptempdir/..., false when an error occured
 360       *                           and null when there is simply nothing.
 361       */
 362      protected function get_restore_content_dir() {
 363          $backupfile = null;
 364          $shortname = null;
 365  
 366          if (!empty($this->options['backupfile'])) {
 367              $backupfile = $this->options['backupfile'];
 368          } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) {
 369              $shortname = $this->options['templatecourse'];
 370          }
 371  
 372          $errors = array();
 373          $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors);
 374          if (!empty($errors)) {
 375              foreach ($errors as $key => $message) {
 376                  $this->error($key, $message);
 377              }
 378              return false;
 379          } else if ($dir === false) {
 380              // We want to return null when nothing was wrong, but nothing was found.
 381              $dir = null;
 382          }
 383  
 384          if (empty($dir) && !empty($this->importoptions['restoredir'])) {
 385              $dir = $this->importoptions['restoredir'];
 386          }
 387  
 388          return $dir;
 389      }
 390  
 391      /**
 392       * Return the errors found during preparation.
 393       *
 394       * @return array
 395       */
 396      public function get_statuses() {
 397          return $this->statuses;
 398      }
 399  
 400      /**
 401       * Return whether there were errors with this course.
 402       *
 403       * @return boolean
 404       */
 405      public function has_errors() {
 406          return !empty($this->errors);
 407      }
 408  
 409      /**
 410       * Validates and prepares the data.
 411       *
 412       * @return bool false is any error occured.
 413       */
 414      public function prepare() {
 415          global $DB, $SITE;
 416          $this->prepared = true;
 417  
 418          // Validate the shortname.
 419          if (!empty($this->shortname) || is_numeric($this->shortname)) {
 420              if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) {
 421                  $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
 422                  return false;
 423              }
 424  
 425              // Ensure we don't overflow the maximum length of the shortname field.
 426              if (core_text::strlen($this->shortname) > 255) {
 427                  $this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255));
 428                  return false;
 429              }
 430          }
 431  
 432          $exists = $this->exists();
 433  
 434          // Do we want to delete the course?
 435          if ($this->options['delete']) {
 436              if (!$exists) {
 437                  $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse'));
 438                  return false;
 439              } else if (!$this->can_delete()) {
 440                  $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse'));
 441                  return false;
 442              }
 443  
 444              $this->do = self::DO_DELETE;
 445              return true;
 446          }
 447  
 448          // Can we create/update the course under those conditions?
 449          if ($exists) {
 450              if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
 451                  $this->error('courseexistsanduploadnotallowed',
 452                      new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
 453                  return false;
 454              } else if ($this->can_update()) {
 455                  // We can never allow for any front page changes!
 456                  if ($this->shortname == $SITE->shortname) {
 457                      $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse'));
 458                      return false;
 459                  }
 460              }
 461          } else {
 462              if (!$this->can_create()) {
 463                  $this->error('coursedoesnotexistandcreatenotallowed',
 464                      new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
 465                  return false;
 466              }
 467          }
 468  
 469          // Basic data.
 470          $coursedata = array();
 471          foreach ($this->rawdata as $field => $value) {
 472              if (!in_array($field, self::$validfields)) {
 473                  continue;
 474              } else if ($field == 'shortname') {
 475                  // Let's leave it apart from now, use $this->shortname only.
 476                  continue;
 477              }
 478              $coursedata[$field] = $value;
 479          }
 480  
 481          $mode = $this->mode;
 482          $updatemode = $this->updatemode;
 483          $usedefaults = $this->can_use_defaults();
 484  
 485          // Resolve the category, and fail if not found.
 486          $errors = array();
 487          $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors);
 488          if (empty($errors)) {
 489              $coursedata['category'] = $catid;
 490          } else {
 491              foreach ($errors as $key => $message) {
 492                  $this->error($key, $message);
 493              }
 494              return false;
 495          }
 496  
 497          // Ensure we don't overflow the maximum length of the fullname field.
 498          if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) {
 499              $this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254));
 500              return false;
 501          }
 502  
 503          // If the course does not exist, or will be forced created.
 504          if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
 505  
 506              // Mandatory fields upon creation.
 507              $errors = array();
 508              foreach (self::$mandatoryfields as $field) {
 509                  if ((!isset($coursedata[$field]) || $coursedata[$field] === '') &&
 510                          (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) {
 511                      $errors[] = $field;
 512                  }
 513              }
 514              if (!empty($errors)) {
 515                  $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse',
 516                      implode(', ', $errors)));
 517                  return false;
 518              }
 519          }
 520  
 521          // Should the course be renamed?
 522          if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) {
 523              if (!$this->can_update()) {
 524                  $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse'));
 525                  return false;
 526              } else if (!$exists) {
 527                  $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse'));
 528                  return false;
 529              } else if (!$this->can_rename()) {
 530                  $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse'));
 531                  return false;
 532              } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) {
 533                  $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
 534                  return false;
 535              } else if ($this->exists($this->options['rename'])) {
 536                  $this->error('cannotrenameshortnamealreadyinuse',
 537                      new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse'));
 538                  return false;
 539              } else if (isset($coursedata['idnumber']) &&
 540                      $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn',
 541                      array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) {
 542                  $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse'));
 543                  return false;
 544              }
 545              $coursedata['shortname'] = $this->options['rename'];
 546              $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse',
 547                  array('from' => $this->shortname, 'to' => $coursedata['shortname'])));
 548          }
 549  
 550          // Should we generate a shortname?
 551          if (empty($this->shortname) && !is_numeric($this->shortname)) {
 552              if (empty($this->importoptions['shortnametemplate'])) {
 553                  $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse'));
 554                  return false;
 555              } else if (!$this->can_only_create()) {
 556                  $this->error('cannotgenerateshortnameupdatemode',
 557                      new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse'));
 558                  return false;
 559              } else {
 560                  $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata,
 561                      $this->importoptions['shortnametemplate']);
 562                  if (is_null($newshortname)) {
 563                      $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse'));
 564                      return false;
 565                  } else if ($this->exists($newshortname)) {
 566                      if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
 567                          $this->error('generatedshortnamealreadyinuse',
 568                              new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse'));
 569                          return false;
 570                      }
 571                      $exists = true;
 572                  }
 573                  $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse',
 574                      $newshortname));
 575                  $this->shortname = $newshortname;
 576              }
 577          }
 578  
 579          // If exists, but we only want to create courses, increment the shortname.
 580          if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
 581              $original = $this->shortname;
 582              $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname);
 583              $exists = false;
 584              if ($this->shortname != $original) {
 585                  $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse',
 586                      array('from' => $original, 'to' => $this->shortname)));
 587                  if (isset($coursedata['idnumber'])) {
 588                      $originalidn = $coursedata['idnumber'];
 589                      $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']);
 590                      if ($originalidn != $coursedata['idnumber']) {
 591                          $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse',
 592                              array('from' => $originalidn, 'to' => $coursedata['idnumber'])));
 593                      }
 594                  }
 595              }
 596          }
 597  
 598          // If the course does not exist, ensure that the ID number is not taken.
 599          if (!$exists && isset($coursedata['idnumber'])) {
 600              if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) {
 601                  $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse'));
 602                  return false;
 603              }
 604          }
 605  
 606          // Course start date.
 607          if (!empty($coursedata['startdate'])) {
 608              $coursedata['startdate'] = strtotime($coursedata['startdate']);
 609          }
 610  
 611          // Course end date.
 612          if (!empty($coursedata['enddate'])) {
 613              $coursedata['enddate'] = strtotime($coursedata['enddate']);
 614          }
 615  
 616          // If lang is specified, check the user is allowed to set that field.
 617          if (!empty($coursedata['lang'])) {
 618              if ($exists) {
 619                  $courseid = $DB->get_field('course', 'id', ['shortname' => $this->shortname]);
 620                  if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($courseid))) {
 621                      $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse'));
 622                      return false;
 623                  }
 624              } else {
 625                  $catcontext = context_coursecat::instance($coursedata['category']);
 626                  if (!guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $catcontext)) {
 627                      $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse'));
 628                      return false;
 629                  }
 630              }
 631          }
 632  
 633          // Ultimate check mode vs. existence.
 634          switch ($mode) {
 635              case tool_uploadcourse_processor::MODE_CREATE_NEW:
 636              case tool_uploadcourse_processor::MODE_CREATE_ALL:
 637                  if ($exists) {
 638                      $this->error('courseexistsanduploadnotallowed',
 639                          new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
 640                      return false;
 641                  }
 642                  break;
 643              case tool_uploadcourse_processor::MODE_UPDATE_ONLY:
 644                  if (!$exists) {
 645                      $this->error('coursedoesnotexistandcreatenotallowed',
 646                          new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
 647                      return false;
 648                  }
 649                  // No break!
 650              case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE:
 651                  if ($exists) {
 652                      if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) {
 653                          $this->error('updatemodedoessettonothing',
 654                              new lang_string('updatemodedoessettonothing', 'tool_uploadcourse'));
 655                          return false;
 656                      }
 657                  }
 658                  break;
 659              default:
 660                  // O_o Huh?! This should really never happen here!
 661                  $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse'));
 662                  return false;
 663          }
 664  
 665          // Get final data.
 666          if ($exists) {
 667              $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS);
 668              $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly);
 669  
 670              // Make sure we are not trying to mess with the front page, though we should never get here!
 671              if ($coursedata['id'] == $SITE->id) {
 672                  $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse'));
 673                  return false;
 674              }
 675  
 676              $this->do = self::DO_UPDATE;
 677          } else {
 678              $coursedata = $this->get_final_create_data($coursedata);
 679              $this->do = self::DO_CREATE;
 680          }
 681  
 682          // Validate course start and end dates.
 683          if ($exists) {
 684              // We also check existing start and end dates if we are updating an existing course.
 685              $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
 686              if (empty($coursedata['startdate'])) {
 687                  $coursedata['startdate'] = $existingdata->startdate;
 688              }
 689              if (empty($coursedata['enddate'])) {
 690                  $coursedata['enddate'] = $existingdata->enddate;
 691              }
 692          }
 693          if ($errorcode = course_validate_dates($coursedata)) {
 694              $this->error($errorcode, new lang_string($errorcode, 'error'));
 695              return false;
 696          }
 697  
 698          // Add role renaming.
 699          $errors = array();
 700          $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors);
 701          if (!empty($errors)) {
 702              foreach ($errors as $key => $message) {
 703                  $this->error($key, $message);
 704              }
 705              return false;
 706          }
 707          foreach ($rolenames as $rolekey => $rolename) {
 708              $coursedata[$rolekey] = $rolename;
 709          }
 710  
 711          // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context.
 712          if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) {
 713              $context = context_course::instance($coursedata['id']);
 714          } else {
 715              // The category ID is taken from the defaults if it exists, otherwise from course data.
 716              $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']);
 717          }
 718          $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context,
 719              $errors);
 720          if (!empty($errors)) {
 721              foreach ($errors as $key => $message) {
 722                  $this->error($key, $message);
 723              }
 724  
 725              return false;
 726          }
 727  
 728          foreach ($customfielddata as $name => $value) {
 729              $coursedata[$name] = $value;
 730          }
 731  
 732          // Some validation.
 733          if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
 734              $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
 735              return false;
 736          }
 737  
 738          // Add data for course format options.
 739          if (isset($coursedata['format']) || $exists) {
 740              if (isset($coursedata['format'])) {
 741                  $courseformat = course_get_format((object)['format' => $coursedata['format']]);
 742              } else {
 743                  $courseformat = course_get_format($existingdata);
 744              }
 745              $coursedata += $courseformat->validate_course_format_options($this->rawdata);
 746          }
 747  
 748          // Special case, 'numsections' is not a course format option any more but still should apply from the template course,
 749          // if any, and otherwise from defaults.
 750          if (!$exists || !array_key_exists('numsections', $coursedata)) {
 751              if (isset($this->rawdata['numsections']) && is_numeric($this->rawdata['numsections'])) {
 752                  $coursedata['numsections'] = (int)$this->rawdata['numsections'];
 753              } else if (isset($this->options['templatecourse'])) {
 754                  $numsections = tool_uploadcourse_helper::get_coursesection_count($this->options['templatecourse']);
 755                  if ($numsections != 0) {
 756                      $coursedata['numsections'] = $numsections;
 757                  } else {
 758                      $coursedata['numsections'] = get_config('moodlecourse', 'numsections');
 759                  }
 760              } else {
 761                  $coursedata['numsections'] = get_config('moodlecourse', 'numsections');
 762              }
 763          }
 764  
 765          // Visibility can only be 0 or 1.
 766          if (!empty($coursedata['visible']) AND !($coursedata['visible'] == 0 OR $coursedata['visible'] == 1)) {
 767              $this->error('invalidvisibilitymode', new lang_string('invalidvisibilitymode', 'tool_uploadcourse'));
 768              return false;
 769          }
 770  
 771          // Saving data.
 772          $this->data = $coursedata;
 773  
 774          // Get enrolment data. Where the course already exists, we can also perform validation.
 775          $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
 776          if ($exists) {
 777              $errors = $this->validate_enrolment_data($coursedata['id'], $this->enrolmentdata);
 778  
 779              if (!empty($errors)) {
 780                  foreach ($errors as $key => $message) {
 781                      $this->error($key, $message);
 782                  }
 783  
 784                  return false;
 785              }
 786          }
 787  
 788          if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') {
 789              $this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY);
 790          }
 791  
 792          // Restore data.
 793          // TODO Speed up things by not really extracting the backup just yet, but checking that
 794          // the backup file or shortname passed are valid. Extraction should happen in proceed().
 795          $this->restoredata = $this->get_restore_content_dir();
 796          if ($this->restoredata === false) {
 797              return false;
 798          }
 799  
 800          // We can only reset courses when allowed and we are updating the course.
 801          if ($this->importoptions['reset'] || $this->options['reset']) {
 802              if ($this->do !== self::DO_UPDATE) {
 803                  $this->error('canonlyresetcourseinupdatemode',
 804                      new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse'));
 805                  return false;
 806              } else if (!$this->can_reset()) {
 807                  $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
 808                  return false;
 809              }
 810          }
 811  
 812          return true;
 813      }
 814  
 815      /**
 816       * Proceed with the import of the course.
 817       *
 818       * @return void
 819       */
 820      public function proceed() {
 821          global $CFG, $USER;
 822  
 823          if (!$this->prepared) {
 824              throw new coding_exception('The course has not been prepared.');
 825          } else if ($this->has_errors()) {
 826              throw new moodle_exception('Cannot proceed, errors were detected.');
 827          } else if ($this->processstarted) {
 828              throw new coding_exception('The process has already been started.');
 829          }
 830          $this->processstarted = true;
 831  
 832          if ($this->do === self::DO_DELETE) {
 833              if ($this->delete()) {
 834                  $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse'));
 835              } else {
 836                  $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse'));
 837              }
 838              return true;
 839          } else if ($this->do === self::DO_CREATE) {
 840              $course = create_course((object) $this->data);
 841              $this->id = $course->id;
 842              $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse'));
 843          } else if ($this->do === self::DO_UPDATE) {
 844              $course = (object) $this->data;
 845              update_course($course);
 846              $this->id = $course->id;
 847              $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse'));
 848          } else {
 849              // Strangely the outcome has not been defined, or is unknown!
 850              throw new coding_exception('Unknown outcome!');
 851          }
 852  
 853          // Restore a course.
 854          if (!empty($this->restoredata)) {
 855              $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO,
 856                  backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
 857  
 858              // Check if the format conversion must happen first.
 859              if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
 860                  $rc->convert();
 861              }
 862              if ($rc->execute_precheck()) {
 863                  $rc->execute_plan();
 864                  $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
 865              } else {
 866                  $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
 867              }
 868              $rc->destroy();
 869          }
 870  
 871          // Proceed with enrolment data.
 872          $this->process_enrolment_data($course);
 873  
 874          // Reset the course.
 875          if ($this->importoptions['reset'] || $this->options['reset']) {
 876              if ($this->do === self::DO_UPDATE && $this->can_reset()) {
 877                  $this->reset($course);
 878                  $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse'));
 879              }
 880          }
 881  
 882          // Mark context as dirty.
 883          $context = context_course::instance($course->id);
 884          $context->mark_dirty();
 885      }
 886  
 887      /**
 888       * Validate passed enrolment data against an existing course
 889       *
 890       * @param int $courseid
 891       * @param array[] $enrolmentdata
 892       * @return lang_string[] Errors keyed on error code
 893       */
 894      protected function validate_enrolment_data(int $courseid, array $enrolmentdata): array {
 895          // Nothing to validate.
 896          if (empty($enrolmentdata)) {
 897              return [];
 898          }
 899  
 900          $errors = [];
 901  
 902          $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
 903          $instances = enrol_get_instances($courseid, false);
 904  
 905          foreach ($enrolmentdata as $method => $options) {
 906              $plugin = $enrolmentplugins[$method];
 907  
 908              // Find matching instances by enrolment method.
 909              $methodinstances = array_filter($instances, static function(stdClass $instance) use ($method) {
 910                  return (strcmp($instance->enrol, $method) == 0);
 911              });
 912  
 913              if (!empty($options['delete'])) {
 914                  // Ensure user is able to delete the instances.
 915                  foreach ($methodinstances as $methodinstance) {
 916                      if (!$plugin->can_delete_instance($methodinstance)) {
 917                          $errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
 918                              $plugin->get_instance_name($methodinstance));
 919  
 920                          break;
 921                      }
 922                  }
 923              } else if (!empty($options['disable'])) {
 924                  // Ensure user is able to toggle instance statuses.
 925                  foreach ($methodinstances as $methodinstance) {
 926                      if (!$plugin->can_hide_show_instance($methodinstance)) {
 927                          $errors['errorcannotdisableenrolment'] =
 928                              new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
 929                                  $plugin->get_instance_name($methodinstance));
 930  
 931                          break;
 932                      }
 933                  }
 934              } else {
 935                  // Ensure user is able to create/update instance.
 936                  $methodinstance = empty($methodinstances) ? null : reset($methodinstances);
 937                  if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) ||
 938                          (!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) {
 939  
 940                      $errors['errorcannotcreateorupdateenrolment'] =
 941                          new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
 942                              $plugin->get_instance_name($methodinstance));
 943  
 944                      break;
 945                  }
 946              }
 947          }
 948  
 949          return $errors;
 950      }
 951  
 952      /**
 953       * Add the enrolment data for the course.
 954       *
 955       * @param object $course course record.
 956       * @return void
 957       */
 958      protected function process_enrolment_data($course) {
 959          global $DB;
 960  
 961          $enrolmentdata = $this->enrolmentdata;
 962          if (empty($enrolmentdata)) {
 963              return;
 964          }
 965  
 966          $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
 967          $instances = enrol_get_instances($course->id, false);
 968          foreach ($enrolmentdata as $enrolmethod => $method) {
 969  
 970              $instance = null;
 971              foreach ($instances as $i) {
 972                  if ($i->enrol == $enrolmethod) {
 973                      $instance = $i;
 974                      break;
 975                  }
 976              }
 977  
 978              $todelete = isset($method['delete']) && $method['delete'];
 979              $todisable = isset($method['disable']) && $method['disable'];
 980              unset($method['delete']);
 981              unset($method['disable']);
 982  
 983              if ($todelete) {
 984                  // Remove the enrolment method.
 985                  if ($instance) {
 986                      $plugin = $enrolmentplugins[$instance->enrol];
 987  
 988                      // Ensure user is able to delete the instance.
 989                      if ($plugin->can_delete_instance($instance)) {
 990                          $plugin->delete_instance($instance);
 991                      } else {
 992                          $this->error('errorcannotdeleteenrolment',
 993                              new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
 994                                  $plugin->get_instance_name($instance)));
 995                      }
 996                  }
 997              } else {
 998                  // Create/update enrolment.
 999                  $plugin = $enrolmentplugins[$enrolmethod];
1000  
1001                  $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED;
1002  
1003                  // Create a new instance if necessary.
1004                  if (empty($instance) && $plugin->can_add_instance($course->id)) {
1005                      $instanceid = $plugin->add_default_instance($course);
1006                      $instance = $DB->get_record('enrol', ['id' => $instanceid]);
1007                      $instance->roleid = $plugin->get_config('roleid');
1008                      // On creation the user can decide the status.
1009                      $plugin->update_status($instance, $status);
1010                  }
1011  
1012                  // Check if the we need to update the instance status.
1013                  if ($instance && $status != $instance->status) {
1014                      if ($plugin->can_hide_show_instance($instance)) {
1015                          $plugin->update_status($instance, $status);
1016                      } else {
1017                          $this->error('errorcannotdisableenrolment',
1018                              new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
1019                                  $plugin->get_instance_name($instance)));
1020                          break;
1021                      }
1022                  }
1023  
1024                  if (empty($instance) || !$plugin->can_edit_instance($instance)) {
1025                      $this->error('errorcannotcreateorupdateenrolment',
1026                          new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
1027                              $plugin->get_instance_name($instance)));
1028  
1029                      break;
1030                  }
1031  
1032                  // Now update values.
1033                  foreach ($method as $k => $v) {
1034                      $instance->{$k} = $v;
1035                  }
1036  
1037                  // Sort out the start, end and date.
1038                  $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0);
1039                  $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0);
1040  
1041                  // Is the enrolment period set?
1042                  if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) {
1043                      if (preg_match('/^\d+$/', $method['enrolperiod'])) {
1044                          $method['enrolperiod'] = (int) $method['enrolperiod'];
1045                      } else {
1046                          // Try and convert period to seconds.
1047                          $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']);
1048                      }
1049                      $instance->enrolperiod = $method['enrolperiod'];
1050                  }
1051                  if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) {
1052                      $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod'];
1053                  }
1054                  if ($instance->enrolenddate > 0) {
1055                      $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
1056                  }
1057                  if ($instance->enrolenddate < $instance->enrolstartdate) {
1058                      $instance->enrolenddate = $instance->enrolstartdate;
1059                  }
1060  
1061                  // Sort out the given role. This does not filter the roles allowed in the course.
1062                  if (isset($method['role'])) {
1063                      $roleids = tool_uploadcourse_helper::get_role_ids();
1064                      if (isset($roleids[$method['role']])) {
1065                          $instance->roleid = $roleids[$method['role']];
1066                      }
1067                  }
1068  
1069                  $instance->timemodified = time();
1070                  $DB->update_record('enrol', $instance);
1071              }
1072          }
1073      }
1074  
1075      /**
1076       * Reset the current course.
1077       *
1078       * This does not reset any of the content of the activities.
1079       *
1080       * @param stdClass $course the course object of the course to reset.
1081       * @return array status array of array component, item, error.
1082       */
1083      protected function reset($course) {
1084          global $DB;
1085  
1086          $resetdata = new stdClass();
1087          $resetdata->id = $course->id;
1088          $resetdata->reset_start_date = time();
1089          $resetdata->reset_events = true;
1090          $resetdata->reset_notes = true;
1091          $resetdata->delete_blog_associations = true;
1092          $resetdata->reset_completion = true;
1093          $resetdata->reset_roles_overrides = true;
1094          $resetdata->reset_roles_local = true;
1095          $resetdata->reset_groups_members = true;
1096          $resetdata->reset_groups_remove = true;
1097          $resetdata->reset_groupings_members = true;
1098          $resetdata->reset_groupings_remove = true;
1099          $resetdata->reset_gradebook_items = true;
1100          $resetdata->reset_gradebook_grades = true;
1101          $resetdata->reset_comments = true;
1102  
1103          if (empty($course->startdate)) {
1104              $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id));
1105          }
1106          $resetdata->reset_start_date_old = $course->startdate;
1107  
1108          if (empty($course->enddate)) {
1109              $course->enddate = $DB->get_field_select('course', 'enddate', 'id = :id', array('id' => $course->id));
1110          }
1111          $resetdata->reset_end_date_old = $course->enddate;
1112  
1113          // Add roles.
1114          $roles = tool_uploadcourse_helper::get_role_ids();
1115          $resetdata->unenrol_users = array_values($roles);
1116          $resetdata->unenrol_users[] = 0;    // Enrolled without role.
1117  
1118          return reset_course_userdata($resetdata);
1119      }
1120  
1121      /**
1122       * Log a status
1123       *
1124       * @param string $code status code.
1125       * @param lang_string $message status message.
1126       * @return void
1127       */
1128      protected function status($code, lang_string $message) {
1129          if (array_key_exists($code, $this->statuses)) {
1130              throw new coding_exception('Status code already defined');
1131          }
1132          $this->statuses[$code] = $message;
1133      }
1134  
1135  }