Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • 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 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [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 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', 'downloadcontent');
      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, $CFG;
     416  
     417          $this->prepared = true;
     418  
     419          // Validate the shortname.
     420          if (!empty($this->shortname) || is_numeric($this->shortname)) {
     421              if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) {
     422                  $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
     423                  return false;
     424              }
     425  
     426              // Ensure we don't overflow the maximum length of the shortname field.
     427              if (core_text::strlen($this->shortname) > 255) {
     428                  $this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255));
     429                  return false;
     430              }
     431          }
     432  
     433          $exists = $this->exists();
     434  
     435          // Do we want to delete the course?
     436          if ($this->options['delete']) {
     437              if (!$exists) {
     438                  $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse'));
     439                  return false;
     440              } else if (!$this->can_delete()) {
     441                  $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse'));
     442                  return false;
     443              }
     444  
     445              $this->do = self::DO_DELETE;
     446              return true;
     447          }
     448  
     449          // Can we create/update the course under those conditions?
     450          if ($exists) {
     451              if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
     452                  $this->error('courseexistsanduploadnotallowed',
     453                      new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
     454                  return false;
     455              } else if ($this->can_update()) {
     456                  // We can never allow for any front page changes!
     457                  if ($this->shortname == $SITE->shortname) {
     458                      $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse'));
     459                      return false;
     460                  }
     461              }
     462          } else {
     463              if (!$this->can_create()) {
     464                  $this->error('coursedoesnotexistandcreatenotallowed',
     465                      new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
     466                  return false;
     467              }
     468          }
     469  
     470          // Basic data.
     471          $coursedata = array();
     472          foreach ($this->rawdata as $field => $value) {
     473              if (!in_array($field, self::$validfields)) {
     474                  continue;
     475              } else if ($field == 'shortname') {
     476                  // Let's leave it apart from now, use $this->shortname only.
     477                  continue;
     478              }
     479              $coursedata[$field] = $value;
     480          }
     481  
     482          $mode = $this->mode;
     483          $updatemode = $this->updatemode;
     484          $usedefaults = $this->can_use_defaults();
     485  
     486          // Resolve the category, and fail if not found.
     487          $errors = array();
     488          $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors);
     489          if (empty($errors)) {
     490              $coursedata['category'] = $catid;
     491          } else {
     492              foreach ($errors as $key => $message) {
     493                  $this->error($key, $message);
     494              }
     495              return false;
     496          }
     497  
     498          // Ensure we don't overflow the maximum length of the fullname field.
     499          if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) {
     500              $this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254));
     501              return false;
     502          }
     503  
     504          // If the course does not exist, or will be forced created.
     505          if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
     506  
     507              // Mandatory fields upon creation.
     508              $errors = array();
     509              foreach (self::$mandatoryfields as $field) {
     510                  if ((!isset($coursedata[$field]) || $coursedata[$field] === '') &&
     511                          (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) {
     512                      $errors[] = $field;
     513                  }
     514              }
     515              if (!empty($errors)) {
     516                  $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse',
     517                      implode(', ', $errors)));
     518                  return false;
     519              }
     520          }
     521  
     522          // Should the course be renamed?
     523          if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) {
     524              if (!$this->can_update()) {
     525                  $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse'));
     526                  return false;
     527              } else if (!$exists) {
     528                  $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse'));
     529                  return false;
     530              } else if (!$this->can_rename()) {
     531                  $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse'));
     532                  return false;
     533              } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) {
     534                  $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
     535                  return false;
     536              } else if ($this->exists($this->options['rename'])) {
     537                  $this->error('cannotrenameshortnamealreadyinuse',
     538                      new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse'));
     539                  return false;
     540              } else if (isset($coursedata['idnumber']) &&
     541                      $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn',
     542                      array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) {
     543                  $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse'));
     544                  return false;
     545              }
     546              $coursedata['shortname'] = $this->options['rename'];
     547              $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse',
     548                  array('from' => $this->shortname, 'to' => $coursedata['shortname'])));
     549          }
     550  
     551          // Should we generate a shortname?
     552          if (empty($this->shortname) && !is_numeric($this->shortname)) {
     553              if (empty($this->importoptions['shortnametemplate'])) {
     554                  $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse'));
     555                  return false;
     556              } else if (!$this->can_only_create()) {
     557                  $this->error('cannotgenerateshortnameupdatemode',
     558                      new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse'));
     559                  return false;
     560              } else {
     561                  $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata,
     562                      $this->importoptions['shortnametemplate']);
     563                  if (is_null($newshortname)) {
     564                      $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse'));
     565                      return false;
     566                  } else if ($this->exists($newshortname)) {
     567                      if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
     568                          $this->error('generatedshortnamealreadyinuse',
     569                              new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse'));
     570                          return false;
     571                      }
     572                      $exists = true;
     573                  }
     574                  $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse',
     575                      $newshortname));
     576                  $this->shortname = $newshortname;
     577              }
     578          }
     579  
     580          // If exists, but we only want to create courses, increment the shortname.
     581          if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
     582              $original = $this->shortname;
     583              $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname);
     584              $exists = false;
     585              if ($this->shortname != $original) {
     586                  $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse',
     587                      array('from' => $original, 'to' => $this->shortname)));
     588                  if (isset($coursedata['idnumber'])) {
     589                      $originalidn = $coursedata['idnumber'];
     590                      $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']);
     591                      if ($originalidn != $coursedata['idnumber']) {
     592                          $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse',
     593                              array('from' => $originalidn, 'to' => $coursedata['idnumber'])));
     594                      }
     595                  }
     596              }
     597          }
     598  
     599          // If the course does not exist, ensure that the ID number is not taken.
     600          if (!$exists && isset($coursedata['idnumber'])) {
     601              if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) {
     602                  $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse'));
     603                  return false;
     604              }
     605          }
     606  
     607          // Course start date.
     608          if (!empty($coursedata['startdate'])) {
     609              $coursedata['startdate'] = strtotime($coursedata['startdate']);
     610          }
     611  
     612          // Course end date.
     613          if (!empty($coursedata['enddate'])) {
     614              $coursedata['enddate'] = strtotime($coursedata['enddate']);
     615          }
     616  
     617          // If lang is specified, check the user is allowed to set that field.
     618          if (!empty($coursedata['lang'])) {
     619              if ($exists) {
     620                  $courseid = $DB->get_field('course', 'id', ['shortname' => $this->shortname]);
     621                  if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($courseid))) {
     622                      $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse'));
     623                      return false;
     624                  }
     625              } else {
     626                  $catcontext = context_coursecat::instance($coursedata['category']);
     627                  if (!guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $catcontext)) {
     628                      $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse'));
     629                      return false;
     630                  }
     631              }
     632          }
     633  
     634          // Ultimate check mode vs. existence.
     635          switch ($mode) {
     636              case tool_uploadcourse_processor::MODE_CREATE_NEW:
     637              case tool_uploadcourse_processor::MODE_CREATE_ALL:
     638                  if ($exists) {
     639                      $this->error('courseexistsanduploadnotallowed',
     640                          new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
     641                      return false;
     642                  }
     643                  break;
     644              case tool_uploadcourse_processor::MODE_UPDATE_ONLY:
     645                  if (!$exists) {
     646                      $this->error('coursedoesnotexistandcreatenotallowed',
     647                          new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
     648                      return false;
     649                  }
     650                  // No break!
     651              case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE:
     652                  if ($exists) {
     653                      if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) {
     654                          $this->error('updatemodedoessettonothing',
     655                              new lang_string('updatemodedoessettonothing', 'tool_uploadcourse'));
     656                          return false;
     657                      }
     658                  }
     659                  break;
     660              default:
     661                  // O_o Huh?! This should really never happen here!
     662                  $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse'));
     663                  return false;
     664          }
     665  
     666          // Get final data.
     667          if ($exists) {
     668              $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS);
     669              $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly);
     670  
     671              // Make sure we are not trying to mess with the front page, though we should never get here!
     672              if ($coursedata['id'] == $SITE->id) {
     673                  $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse'));
     674                  return false;
     675              }
     676  
     677              $this->do = self::DO_UPDATE;
     678          } else {
     679              $coursedata = $this->get_final_create_data($coursedata);
     680              $this->do = self::DO_CREATE;
     681          }
     682  
     683          // Validate course start and end dates.
     684          if ($exists) {
     685              // We also check existing start and end dates if we are updating an existing course.
     686              $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
     687              if (empty($coursedata['startdate'])) {
     688                  $coursedata['startdate'] = $existingdata->startdate;
     689              }
     690              if (empty($coursedata['enddate'])) {
     691                  $coursedata['enddate'] = $existingdata->enddate;
     692              }
     693          }
     694          if ($errorcode = course_validate_dates($coursedata)) {
     695              $this->error($errorcode, new lang_string($errorcode, 'error'));
     696              return false;
     697          }
     698  
     699          // Add role renaming.
     700          $errors = array();
     701          $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors);
     702          if (!empty($errors)) {
     703              foreach ($errors as $key => $message) {
     704                  $this->error($key, $message);
     705              }
     706              return false;
     707          }
     708          foreach ($rolenames as $rolekey => $rolename) {
     709              $coursedata[$rolekey] = $rolename;
     710          }
     711  
     712          // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context.
     713          if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) {
     714              $context = context_course::instance($coursedata['id']);
     715          } else {
     716              // The category ID is taken from the defaults if it exists, otherwise from course data.
     717              $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']);
     718          }
     719          $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context,
     720              $errors);
     721          if (!empty($errors)) {
     722              foreach ($errors as $key => $message) {
     723                  $this->error($key, $message);
     724              }
     725  
     726              return false;
     727          }
     728  
     729          foreach ($customfielddata as $name => $value) {
     730              $coursedata[$name] = $value;
     731          }
     732  
     733          // Some validation.
     734          if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
     735              $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
     736              return false;
     737          }
     738  
     739          // Add data for course format options.
     740          if (isset($coursedata['format']) || $exists) {
     741              if (isset($coursedata['format'])) {
     742                  $courseformat = course_get_format((object)['format' => $coursedata['format']]);
     743              } else {
     744                  $courseformat = course_get_format($existingdata);
     745              }
     746              $coursedata += $courseformat->validate_course_format_options($this->rawdata);
     747          }
     748  
     749          // Special case, 'numsections' is not a course format option any more but still should apply from the template course,
     750          // if any, and otherwise from defaults.
     751          if (!$exists || !array_key_exists('numsections', $coursedata)) {
     752              if (isset($this->rawdata['numsections']) && is_numeric($this->rawdata['numsections'])) {
     753                  $coursedata['numsections'] = (int)$this->rawdata['numsections'];
     754              } else if (isset($this->options['templatecourse'])) {
     755                  $numsections = tool_uploadcourse_helper::get_coursesection_count($this->options['templatecourse']);
     756                  if ($numsections != 0) {
     757                      $coursedata['numsections'] = $numsections;
     758                  } else {
     759                      $coursedata['numsections'] = get_config('moodlecourse', 'numsections');
     760                  }
     761              } else {
     762                  $coursedata['numsections'] = get_config('moodlecourse', 'numsections');
     763              }
     764          }
     765  
     766          // Visibility can only be 0 or 1.
     767          if (!empty($coursedata['visible']) AND !($coursedata['visible'] == 0 OR $coursedata['visible'] == 1)) {
     768              $this->error('invalidvisibilitymode', new lang_string('invalidvisibilitymode', 'tool_uploadcourse'));
     769              return false;
     770          }
     771  
     772          // Ensure that user is allowed to configure course content download and the field contains a valid value.
     773          if (isset($coursedata['downloadcontent'])) {
     774              if (!$CFG->downloadcoursecontentallowed ||
     775                      !has_capability('moodle/course:configuredownloadcontent', $context)) {
     776  
     777                  $this->error('downloadcontentnotallowed', new lang_string('downloadcontentnotallowed', 'tool_uploadcourse'));
     778                  return false;
     779              }
     780  
     781              $downloadcontentvalues = [
     782                  DOWNLOAD_COURSE_CONTENT_DISABLED,
     783                  DOWNLOAD_COURSE_CONTENT_ENABLED,
     784                  DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT,
     785              ];
     786              if (!in_array($coursedata['downloadcontent'], $downloadcontentvalues)) {
     787                  $this->error('invaliddownloadcontent', new lang_string('invaliddownloadcontent', 'tool_uploadcourse'));
     788                  return false;
     789              }
     790          }
     791  
     792          // Saving data.
     793          $this->data = $coursedata;
     794  
     795          // Get enrolment data. Where the course already exists, we can also perform validation.
     796          $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
     797          if ($exists) {
     798              $errors = $this->validate_enrolment_data($coursedata['id'], $this->enrolmentdata);
     799  
     800              if (!empty($errors)) {
     801                  foreach ($errors as $key => $message) {
     802                      $this->error($key, $message);
     803                  }
     804  
     805                  return false;
     806              }
     807          }
     808  
     809          if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') {
     810              $this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY);
     811          }
     812  
     813          // Restore data.
     814          // TODO Speed up things by not really extracting the backup just yet, but checking that
     815          // the backup file or shortname passed are valid. Extraction should happen in proceed().
     816          $this->restoredata = $this->get_restore_content_dir();
     817          if ($this->restoredata === false) {
     818              return false;
     819          }
     820  
     821          // We can only reset courses when allowed and we are updating the course.
     822          if ($this->importoptions['reset'] || $this->options['reset']) {
     823              if ($this->do !== self::DO_UPDATE) {
     824                  $this->error('canonlyresetcourseinupdatemode',
     825                      new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse'));
     826                  return false;
     827              } else if (!$this->can_reset()) {
     828                  $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
     829                  return false;
     830              }
     831          }
     832  
     833          return true;
     834      }
     835  
     836      /**
     837       * Proceed with the import of the course.
     838       *
     839       * @return void
     840       */
     841      public function proceed() {
     842          global $CFG, $USER;
     843  
     844          if (!$this->prepared) {
     845              throw new coding_exception('The course has not been prepared.');
     846          } else if ($this->has_errors()) {
     847              throw new moodle_exception('Cannot proceed, errors were detected.');
     848          } else if ($this->processstarted) {
     849              throw new coding_exception('The process has already been started.');
     850          }
     851          $this->processstarted = true;
     852  
     853          if ($this->do === self::DO_DELETE) {
     854              if ($this->delete()) {
     855                  $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse'));
     856              } else {
     857                  $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse'));
     858              }
     859              return true;
     860          } else if ($this->do === self::DO_CREATE) {
     861              $course = create_course((object) $this->data);
     862              $this->id = $course->id;
     863              $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse'));
     864          } else if ($this->do === self::DO_UPDATE) {
     865              $course = (object) $this->data;
     866              update_course($course);
     867              $this->id = $course->id;
     868              $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse'));
     869          } else {
     870              // Strangely the outcome has not been defined, or is unknown!
     871              throw new coding_exception('Unknown outcome!');
     872          }
     873  
     874          // Restore a course.
     875          if (!empty($this->restoredata)) {
     876              $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO,
     877                  backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
     878  
     879              // Check if the format conversion must happen first.
     880              if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
     881                  $rc->convert();
     882              }
     883              if ($rc->execute_precheck()) {
     884                  $rc->execute_plan();
     885                  $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
     886              } else {
     887                  $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
     888              }
     889              $rc->destroy();
     890          }
     891  
     892          // Proceed with enrolment data.
     893          $this->process_enrolment_data($course);
     894  
     895          // Reset the course.
     896          if ($this->importoptions['reset'] || $this->options['reset']) {
     897              if ($this->do === self::DO_UPDATE && $this->can_reset()) {
     898                  $this->reset($course);
     899                  $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse'));
     900              }
     901          }
     902  
     903          // Mark context as dirty.
     904          $context = context_course::instance($course->id);
     905          $context->mark_dirty();
     906      }
     907  
     908      /**
     909       * Validate passed enrolment data against an existing course
     910       *
     911       * @param int $courseid
     912       * @param array[] $enrolmentdata
     913       * @return lang_string[] Errors keyed on error code
     914       */
     915      protected function validate_enrolment_data(int $courseid, array $enrolmentdata): array {
     916          // Nothing to validate.
     917          if (empty($enrolmentdata)) {
     918              return [];
     919          }
     920  
     921          $errors = [];
     922  
     923          $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
     924          $instances = enrol_get_instances($courseid, false);
     925  
     926          foreach ($enrolmentdata as $method => $options) {
     927              $plugin = $enrolmentplugins[$method];
     928  
     929              // Find matching instances by enrolment method.
     930              $methodinstances = array_filter($instances, static function(stdClass $instance) use ($method) {
     931                  return (strcmp($instance->enrol, $method) == 0);
     932              });
     933  
     934              if (!empty($options['delete'])) {
     935                  // Ensure user is able to delete the instances.
     936                  foreach ($methodinstances as $methodinstance) {
     937                      if (!$plugin->can_delete_instance($methodinstance)) {
     938                          $errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
     939                              $plugin->get_instance_name($methodinstance));
     940  
     941                          break;
     942                      }
     943                  }
     944              } else if (!empty($options['disable'])) {
     945                  // Ensure user is able to toggle instance statuses.
     946                  foreach ($methodinstances as $methodinstance) {
     947                      if (!$plugin->can_hide_show_instance($methodinstance)) {
     948                          $errors['errorcannotdisableenrolment'] =
     949                              new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
     950                                  $plugin->get_instance_name($methodinstance));
     951  
     952                          break;
     953                      }
     954                  }
     955              } else {
     956                  // Ensure user is able to create/update instance.
     957                  $methodinstance = empty($methodinstances) ? null : reset($methodinstances);
     958                  if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) ||
     959                          (!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) {
     960  
     961                      $errors['errorcannotcreateorupdateenrolment'] =
     962                          new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
     963                              $plugin->get_instance_name($methodinstance));
     964  
     965                      break;
     966                  }
     967              }
     968          }
     969  
     970          return $errors;
     971      }
     972  
     973      /**
     974       * Add the enrolment data for the course.
     975       *
     976       * @param object $course course record.
     977       * @return void
     978       */
     979      protected function process_enrolment_data($course) {
     980          global $DB;
     981  
     982          $enrolmentdata = $this->enrolmentdata;
     983          if (empty($enrolmentdata)) {
     984              return;
     985          }
     986  
     987          $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
     988          $instances = enrol_get_instances($course->id, false);
     989          foreach ($enrolmentdata as $enrolmethod => $method) {
     990  
     991              $instance = null;
     992              foreach ($instances as $i) {
     993                  if ($i->enrol == $enrolmethod) {
     994                      $instance = $i;
     995                      break;
     996                  }
     997              }
     998  
     999              $todelete = isset($method['delete']) && $method['delete'];
    1000              $todisable = isset($method['disable']) && $method['disable'];
    1001              unset($method['delete']);
    1002              unset($method['disable']);
    1003  
    1004              if ($todelete) {
    1005                  // Remove the enrolment method.
    1006                  if ($instance) {
    1007                      $plugin = $enrolmentplugins[$instance->enrol];
    1008  
    1009                      // Ensure user is able to delete the instance.
    1010                      if ($plugin->can_delete_instance($instance)) {
    1011                          $plugin->delete_instance($instance);
    1012                      } else {
    1013                          $this->error('errorcannotdeleteenrolment',
    1014                              new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
    1015                                  $plugin->get_instance_name($instance)));
    1016                      }
    1017                  }
    1018              } else {
    1019                  // Create/update enrolment.
    1020                  $plugin = $enrolmentplugins[$enrolmethod];
    1021  
    1022                  $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED;
    1023  
    1024                  // Create a new instance if necessary.
    1025                  if (empty($instance) && $plugin->can_add_instance($course->id)) {
    1026                      $instanceid = $plugin->add_default_instance($course);
    1027                      $instance = $DB->get_record('enrol', ['id' => $instanceid]);
    1028                      $instance->roleid = $plugin->get_config('roleid');
    1029                      // On creation the user can decide the status.
    1030                      $plugin->update_status($instance, $status);
    1031                  }
    1032  
    1033                  // Check if the we need to update the instance status.
    1034                  if ($instance && $status != $instance->status) {
    1035                      if ($plugin->can_hide_show_instance($instance)) {
    1036                          $plugin->update_status($instance, $status);
    1037                      } else {
    1038                          $this->error('errorcannotdisableenrolment',
    1039                              new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
    1040                                  $plugin->get_instance_name($instance)));
    1041                          break;
    1042                      }
    1043                  }
    1044  
    1045                  if (empty($instance) || !$plugin->can_edit_instance($instance)) {
    1046                      $this->error('errorcannotcreateorupdateenrolment',
    1047                          new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
    1048                              $plugin->get_instance_name($instance)));
    1049  
    1050                      break;
    1051                  }
    1052  
    1053                  // Now update values.
    1054                  foreach ($method as $k => $v) {
    1055                      $instance->{$k} = $v;
    1056                  }
    1057  
    1058                  // Sort out the start, end and date.
    1059                  $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0);
    1060                  $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0);
    1061  
    1062                  // Is the enrolment period set?
    1063                  if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) {
    1064                      if (preg_match('/^\d+$/', $method['enrolperiod'])) {
    1065                          $method['enrolperiod'] = (int) $method['enrolperiod'];
    1066                      } else {
    1067                          // Try and convert period to seconds.
    1068                          $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']);
    1069                      }
    1070                      $instance->enrolperiod = $method['enrolperiod'];
    1071                  }
    1072                  if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) {
    1073                      $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod'];
    1074                  }
    1075                  if ($instance->enrolenddate > 0) {
    1076                      $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
    1077                  }
    1078                  if ($instance->enrolenddate < $instance->enrolstartdate) {
    1079                      $instance->enrolenddate = $instance->enrolstartdate;
    1080                  }
    1081  
    1082                  // Sort out the given role. This does not filter the roles allowed in the course.
    1083                  if (isset($method['role'])) {
    1084                      $roleids = tool_uploadcourse_helper::get_role_ids();
    1085                      if (isset($roleids[$method['role']])) {
    1086                          $instance->roleid = $roleids[$method['role']];
    1087                      }
    1088                  }
    1089  
    1090                  $instance->timemodified = time();
    1091                  $DB->update_record('enrol', $instance);
    1092              }
    1093          }
    1094      }
    1095  
    1096      /**
    1097       * Reset the current course.
    1098       *
    1099       * This does not reset any of the content of the activities.
    1100       *
    1101       * @param stdClass $course the course object of the course to reset.
    1102       * @return array status array of array component, item, error.
    1103       */
    1104      protected function reset($course) {
    1105          global $DB;
    1106  
    1107          $resetdata = new stdClass();
    1108          $resetdata->id = $course->id;
    1109          $resetdata->reset_start_date = time();
    1110          $resetdata->reset_events = true;
    1111          $resetdata->reset_notes = true;
    1112          $resetdata->delete_blog_associations = true;
    1113          $resetdata->reset_completion = true;
    1114          $resetdata->reset_roles_overrides = true;
    1115          $resetdata->reset_roles_local = true;
    1116          $resetdata->reset_groups_members = true;
    1117          $resetdata->reset_groups_remove = true;
    1118          $resetdata->reset_groupings_members = true;
    1119          $resetdata->reset_groupings_remove = true;
    1120          $resetdata->reset_gradebook_items = true;
    1121          $resetdata->reset_gradebook_grades = true;
    1122          $resetdata->reset_comments = true;
    1123  
    1124          if (empty($course->startdate)) {
    1125              $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id));
    1126          }
    1127          $resetdata->reset_start_date_old = $course->startdate;
    1128  
    1129          if (empty($course->enddate)) {
    1130              $course->enddate = $DB->get_field_select('course', 'enddate', 'id = :id', array('id' => $course->id));
    1131          }
    1132          $resetdata->reset_end_date_old = $course->enddate;
    1133  
    1134          // Add roles.
    1135          $roles = tool_uploadcourse_helper::get_role_ids();
    1136          $resetdata->unenrol_users = array_values($roles);
    1137          $resetdata->unenrol_users[] = 0;    // Enrolled without role.
    1138  
    1139          return reset_course_userdata($resetdata);
    1140      }
    1141  
    1142      /**
    1143       * Log a status
    1144       *
    1145       * @param string $code status code.
    1146       * @param lang_string $message status message.
    1147       * @return void
    1148       */
    1149      protected function status($code, lang_string $message) {
    1150          if (array_key_exists($code, $this->statuses)) {
    1151              throw new coding_exception('Status code already defined');
    1152          }
    1153          $this->statuses[$code] = $message;
    1154      }
    1155  
    1156  }