Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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