Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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   * 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          $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
 774  
 775          if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') {
 776              $this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY);
 777          }
 778  
 779          // Restore data.
 780          // TODO Speed up things by not really extracting the backup just yet, but checking that
 781          // the backup file or shortname passed are valid. Extraction should happen in proceed().
 782          $this->restoredata = $this->get_restore_content_dir();
 783          if ($this->restoredata === false) {
 784              return false;
 785          }
 786  
 787          // We can only reset courses when allowed and we are updating the course.
 788          if ($this->importoptions['reset'] || $this->options['reset']) {
 789              if ($this->do !== self::DO_UPDATE) {
 790                  $this->error('canonlyresetcourseinupdatemode',
 791                      new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse'));
 792                  return false;
 793              } else if (!$this->can_reset()) {
 794                  $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
 795                  return false;
 796              }
 797          }
 798  
 799          return true;
 800      }
 801  
 802      /**
 803       * Proceed with the import of the course.
 804       *
 805       * @return void
 806       */
 807      public function proceed() {
 808          global $CFG, $USER;
 809  
 810          if (!$this->prepared) {
 811              throw new coding_exception('The course has not been prepared.');
 812          } else if ($this->has_errors()) {
 813              throw new moodle_exception('Cannot proceed, errors were detected.');
 814          } else if ($this->processstarted) {
 815              throw new coding_exception('The process has already been started.');
 816          }
 817          $this->processstarted = true;
 818  
 819          if ($this->do === self::DO_DELETE) {
 820              if ($this->delete()) {
 821                  $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse'));
 822              } else {
 823                  $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse'));
 824              }
 825              return true;
 826          } else if ($this->do === self::DO_CREATE) {
 827              $course = create_course((object) $this->data);
 828              $this->id = $course->id;
 829              $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse'));
 830          } else if ($this->do === self::DO_UPDATE) {
 831              $course = (object) $this->data;
 832              update_course($course);
 833              $this->id = $course->id;
 834              $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse'));
 835          } else {
 836              // Strangely the outcome has not been defined, or is unknown!
 837              throw new coding_exception('Unknown outcome!');
 838          }
 839  
 840          // Restore a course.
 841          if (!empty($this->restoredata)) {
 842              $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO,
 843                  backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
 844  
 845              // Check if the format conversion must happen first.
 846              if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
 847                  $rc->convert();
 848              }
 849              if ($rc->execute_precheck()) {
 850                  $rc->execute_plan();
 851                  $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
 852              } else {
 853                  $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
 854              }
 855              $rc->destroy();
 856          }
 857  
 858          // Proceed with enrolment data.
 859          $this->process_enrolment_data($course);
 860  
 861          // Reset the course.
 862          if ($this->importoptions['reset'] || $this->options['reset']) {
 863              if ($this->do === self::DO_UPDATE && $this->can_reset()) {
 864                  $this->reset($course);
 865                  $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse'));
 866              }
 867          }
 868  
 869          // Mark context as dirty.
 870          $context = context_course::instance($course->id);
 871          $context->mark_dirty();
 872      }
 873  
 874      /**
 875       * Add the enrolment data for the course.
 876       *
 877       * @param object $course course record.
 878       * @return void
 879       */
 880      protected function process_enrolment_data($course) {
 881          global $DB;
 882  
 883          $enrolmentdata = $this->enrolmentdata;
 884          if (empty($enrolmentdata)) {
 885              return;
 886          }
 887  
 888          $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
 889          $instances = enrol_get_instances($course->id, false);
 890          foreach ($enrolmentdata as $enrolmethod => $method) {
 891  
 892              $instance = null;
 893              foreach ($instances as $i) {
 894                  if ($i->enrol == $enrolmethod) {
 895                      $instance = $i;
 896                      break;
 897                  }
 898              }
 899  
 900              $todelete = isset($method['delete']) && $method['delete'];
 901              $todisable = isset($method['disable']) && $method['disable'];
 902              unset($method['delete']);
 903              unset($method['disable']);
 904  
 905              if ($todelete) {
 906                  // Remove the enrolment method.
 907                  if ($instance) {
 908                      $plugin = $enrolmentplugins[$instance->enrol];
 909                      $plugin->delete_instance($instance);
 910                  }
 911              } else if (!empty($instance) && $todisable) {
 912                  // Disable the enrolment.
 913                  $plugin = $enrolmentplugins[$instance->enrol];
 914                  $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
 915                  $enrol_updated = true;
 916              } else {
 917                  $plugin = null;
 918  
 919                  $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED;
 920  
 921                  if (empty($instance)) {
 922                      $plugin = $enrolmentplugins[$enrolmethod];
 923                      $instanceid = $plugin->add_default_instance($course);
 924                      $instance = $DB->get_record('enrol', ['id' => $instanceid]);
 925                      $instance->roleid = $plugin->get_config('roleid');
 926                      $plugin->update_status($instance, $status);
 927                  } else {
 928                      $plugin = $enrolmentplugins[$instance->enrol];
 929                      $plugin->update_status($instance, $status);
 930                  }
 931  
 932                  // Now update values.
 933                  foreach ($method as $k => $v) {
 934                      $instance->{$k} = $v;
 935                  }
 936  
 937                  // Sort out the start, end and date.
 938                  $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0);
 939                  $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0);
 940  
 941                  // Is the enrolment period set?
 942                  if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) {
 943                      if (preg_match('/^\d+$/', $method['enrolperiod'])) {
 944                          $method['enrolperiod'] = (int) $method['enrolperiod'];
 945                      } else {
 946                          // Try and convert period to seconds.
 947                          $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']);
 948                      }
 949                      $instance->enrolperiod = $method['enrolperiod'];
 950                  }
 951                  if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) {
 952                      $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod'];
 953                  }
 954                  if ($instance->enrolenddate > 0) {
 955                      $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
 956                  }
 957                  if ($instance->enrolenddate < $instance->enrolstartdate) {
 958                      $instance->enrolenddate = $instance->enrolstartdate;
 959                  }
 960  
 961                  // Sort out the given role. This does not filter the roles allowed in the course.
 962                  if (isset($method['role'])) {
 963                      $roleids = tool_uploadcourse_helper::get_role_ids();
 964                      if (isset($roleids[$method['role']])) {
 965                          $instance->roleid = $roleids[$method['role']];
 966                      }
 967                  }
 968  
 969                  $instance->timemodified = time();
 970                  $DB->update_record('enrol', $instance);
 971              }
 972          }
 973      }
 974  
 975      /**
 976       * Reset the current course.
 977       *
 978       * This does not reset any of the content of the activities.
 979       *
 980       * @param stdClass $course the course object of the course to reset.
 981       * @return array status array of array component, item, error.
 982       */
 983      protected function reset($course) {
 984          global $DB;
 985  
 986          $resetdata = new stdClass();
 987          $resetdata->id = $course->id;
 988          $resetdata->reset_start_date = time();
 989          $resetdata->reset_events = true;
 990          $resetdata->reset_notes = true;
 991          $resetdata->delete_blog_associations = true;
 992          $resetdata->reset_completion = true;
 993          $resetdata->reset_roles_overrides = true;
 994          $resetdata->reset_roles_local = true;
 995          $resetdata->reset_groups_members = true;
 996          $resetdata->reset_groups_remove = true;
 997          $resetdata->reset_groupings_members = true;
 998          $resetdata->reset_groupings_remove = true;
 999          $resetdata->reset_gradebook_items = true;
1000          $resetdata->reset_gradebook_grades = true;
1001          $resetdata->reset_comments = true;
1002  
1003          if (empty($course->startdate)) {
1004              $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id));
1005          }
1006          $resetdata->reset_start_date_old = $course->startdate;
1007  
1008          if (empty($course->enddate)) {
1009              $course->enddate = $DB->get_field_select('course', 'enddate', 'id = :id', array('id' => $course->id));
1010          }
1011          $resetdata->reset_end_date_old = $course->enddate;
1012  
1013          // Add roles.
1014          $roles = tool_uploadcourse_helper::get_role_ids();
1015          $resetdata->unenrol_users = array_values($roles);
1016          $resetdata->unenrol_users[] = 0;    // Enrolled without role.
1017  
1018          return reset_course_userdata($resetdata);
1019      }
1020  
1021      /**
1022       * Log a status
1023       *
1024       * @param string $code status code.
1025       * @param lang_string $message status message.
1026       * @return void
1027       */
1028      protected function status($code, lang_string $message) {
1029          if (array_key_exists($code, $this->statuses)) {
1030              throw new coding_exception('Status code already defined');
1031          }
1032          $this->statuses[$code] = $message;
1033      }
1034  
1035  }