Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * File containing the helper 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 . '/cache/lib.php');
  27  require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
  28  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  29  
  30  /**
  31   * Class containing a set of helpers.
  32   *
  33   * @package    tool_uploadcourse
  34   * @copyright  2013 Frédéric Massart
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class tool_uploadcourse_helper {
  38  
  39      /**
  40       * Generate a shortname based on a template.
  41       *
  42       * @param array|object $data course data.
  43       * @param string $templateshortname template of shortname.
  44       * @return null|string shortname based on the template, or null when an error occured.
  45       */
  46      public static function generate_shortname($data, $templateshortname) {
  47          if (empty($templateshortname) && !is_numeric($templateshortname)) {
  48              return null;
  49          }
  50          if (strpos($templateshortname, '%') === false) {
  51              return $templateshortname;
  52          }
  53  
  54          $course = (object) $data;
  55          $fullname   = isset($course->fullname) ? $course->fullname : '';
  56          $idnumber   = isset($course->idnumber) ? $course->idnumber  : '';
  57  
  58          $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
  59          $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
  60  
  61          if (!is_null($result)) {
  62              $result = clean_param($result, PARAM_TEXT);
  63          }
  64  
  65          if (empty($result) && !is_numeric($result)) {
  66              $result = null;
  67          }
  68  
  69          return $result;
  70      }
  71  
  72      /**
  73       * Callback used when generating a shortname based on a template.
  74       *
  75       * @param string $fullname full name.
  76       * @param string $idnumber ID number.
  77       * @param array $block result from preg_replace_callback.
  78       * @return string
  79       */
  80      public static function generate_shortname_callback($fullname, $idnumber, $block) {
  81          switch ($block[3]) {
  82              case 'f':
  83                  $repl = $fullname;
  84                  break;
  85              case 'i':
  86                  $repl = $idnumber;
  87                  break;
  88              default:
  89                  return $block[0];
  90          }
  91  
  92          switch ($block[1]) {
  93              case '+':
  94                  $repl = core_text::strtoupper($repl);
  95                  break;
  96              case '-':
  97                  $repl = core_text::strtolower($repl);
  98                  break;
  99              case '~':
 100                  $repl = core_text::strtotitle($repl);
 101                  break;
 102          }
 103  
 104          if (!empty($block[2])) {
 105              $repl = core_text::substr($repl, 0, $block[2]);
 106          }
 107  
 108          return $repl;
 109      }
 110  
 111      /**
 112       * Return the available course formats.
 113       *
 114       * @return array
 115       */
 116      public static function get_course_formats() {
 117          return array_keys(core_component::get_plugin_list('format'));
 118      }
 119  
 120      /**
 121       * Extract enrolment data from passed data.
 122       *
 123       * Constructs an array of methods, and their options:
 124       * array(
 125       *     'method1' => array(
 126       *         'option1' => value,
 127       *         'option2' => value
 128       *     ),
 129       *     'method2' => array(
 130       *         'option1' => value,
 131       *         'option2' => value
 132       *     )
 133       * )
 134       *
 135       * @param array $data data to extract the enrolment data from.
 136       * @return array
 137       */
 138      public static function get_enrolment_data($data) {
 139          $enrolmethods = array();
 140          $enroloptions = array();
 141          foreach ($data as $field => $value) {
 142  
 143              // Enrolmnent data.
 144              $matches = array();
 145              if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
 146                  $key = $matches[1];
 147                  if (!isset($enroloptions[$key])) {
 148                      $enroloptions[$key] = array();
 149                  }
 150                  if (empty($matches[3])) {
 151                      $enrolmethods[$key] = $value;
 152                  } else {
 153                      $enroloptions[$key][$matches[3]] = $value;
 154                  }
 155              }
 156          }
 157  
 158          // Combining enrolment methods and their options in a single array.
 159          $enrolmentdata = array();
 160          if (!empty($enrolmethods)) {
 161              $enrolmentplugins = self::get_enrolment_plugins();
 162              foreach ($enrolmethods as $key => $method) {
 163                  if (!array_key_exists($method, $enrolmentplugins)) {
 164                      // Error!
 165                      continue;
 166                  }
 167                  $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
 168              }
 169          }
 170          return $enrolmentdata;
 171      }
 172  
 173      /**
 174       * Return the enrolment plugins.
 175       *
 176       * The result is cached for faster execution.
 177       *
 178       * @return array
 179       */
 180      public static function get_enrolment_plugins() {
 181          $cache = cache::make('tool_uploadcourse', 'helper');
 182          if (($enrol = $cache->get('enrol')) === false) {
 183              $enrol = enrol_get_plugins(false);
 184              $cache->set('enrol', $enrol);
 185          }
 186          return $enrol;
 187      }
 188  
 189      /**
 190       * Get the restore content tempdir.
 191       *
 192       * The tempdir is the sub directory in which the backup has been extracted.
 193       *
 194       * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
 195       * needs to be enabled, otherwise the cache is ignored.
 196       *
 197       * @param string $backupfile path to a backup file.
 198       * @param string $shortname shortname of a course.
 199       * @param array $errors will be populated with errors found.
 200       * @return string|false false when the backup couldn't retrieved.
 201       */
 202      public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
 203          global $CFG, $DB, $USER;
 204  
 205          $cachekey = null;
 206          if (!empty($backupfile)) {
 207              $backupfile = realpath($backupfile);
 208              if (empty($backupfile) || !is_readable($backupfile)) {
 209                  $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
 210                  return false;
 211              }
 212              $cachekey = 'backup_path:' . $backupfile;
 213          } else if (!empty($shortname) || is_numeric($shortname)) {
 214              $cachekey = 'backup_sn:' . $shortname;
 215          }
 216  
 217          if (empty($cachekey)) {
 218              return false;
 219          }
 220  
 221          // If $CFG->keeptempdirectoriesonbackup is not set to true, any restore happening would
 222          // automatically delete the backup directory... causing the cache to return an unexisting directory.
 223          $usecache = !empty($CFG->keeptempdirectoriesonbackup);
 224          if ($usecache) {
 225              $cache = cache::make('tool_uploadcourse', 'helper');
 226          }
 227  
 228          // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
 229          if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir(get_backup_temp_directory($backupid)))) {
 230  
 231              // Use null instead of false because it would consider that the cache key has not been set.
 232              $backupid = null;
 233  
 234              if (!empty($backupfile)) {
 235                  // Extracting the backup file.
 236                  $packer = get_file_packer('application/vnd.moodle.backup');
 237                  $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
 238                  $path = make_backup_temp_directory($backupid, false);
 239                  $result = $packer->extract_to_pathname($backupfile, $path);
 240                  if (!$result) {
 241                      $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
 242                  }
 243              } else if (!empty($shortname) || is_numeric($shortname)) {
 244                  // Creating restore from an existing course.
 245                  $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
 246                  if (!empty($courseid)) {
 247                      $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
 248                          backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
 249                      $bc->execute_plan();
 250                      $backupid = $bc->get_backupid();
 251                      $bc->destroy();
 252                  } else {
 253                      $errors['coursetorestorefromdoesnotexist'] =
 254                          new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
 255                  }
 256              }
 257  
 258              if ($usecache) {
 259                  $cache->set($cachekey, $backupid);
 260              }
 261          }
 262  
 263          if ($backupid === null) {
 264              $backupid = false;
 265          }
 266          return $backupid;
 267      }
 268  
 269      /**
 270       * Return the role IDs.
 271       *
 272       * The result is cached for faster execution.
 273       *
 274       * @return array
 275       */
 276      public static function get_role_ids() {
 277          $cache = cache::make('tool_uploadcourse', 'helper');
 278          if (($roles = $cache->get('roles')) === false) {
 279              $roles = array();
 280              $rolesraw = get_all_roles();
 281              foreach ($rolesraw as $role) {
 282                  $roles[$role->shortname] = $role->id;
 283              }
 284              $cache->set('roles', $roles);
 285          }
 286          return $roles;
 287      }
 288  
 289      /**
 290       * Helper to detect how many sections a course with a given shortname has.
 291       *
 292       * @param string $shortname shortname of a course to count sections from.
 293       * @return integer count of sections.
 294       */
 295      public static function get_coursesection_count($shortname) {
 296          global $DB;
 297          if (!empty($shortname) || is_numeric($shortname)) {
 298              // Creating restore from an existing course.
 299              $course = $DB->get_record('course', array('shortname' => $shortname));
 300          }
 301          if (!empty($course)) {
 302              $courseformat = course_get_format($course);
 303              return $courseformat->get_last_section_number();
 304          }
 305          return 0;
 306      }
 307  
 308      /**
 309       * Get the role renaming data from the passed data.
 310       *
 311       * @param array $data data to extract the names from.
 312       * @param array $errors will be populated with errors found.
 313       * @return array where the key is the role_<id>, the value is the new name.
 314       */
 315      public static function get_role_names($data, &$errors = array()) {
 316          $rolenames = array();
 317          $rolesids = self::get_role_ids();
 318          $invalidroles = array();
 319          foreach ($data as $field => $value) {
 320  
 321              $matches = array();
 322              if (preg_match('/^role_(.+)?$/', $field, $matches)) {
 323                  if (!isset($rolesids[$matches[1]])) {
 324                      $invalidroles[] = $matches[1];
 325                      continue;
 326                  }
 327                  $rolenames['role_' . $rolesids[$matches[1]]] = $value;
 328              }
 329  
 330          }
 331  
 332          if (!empty($invalidroles)) {
 333              $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
 334          }
 335  
 336          // Roles names.
 337          return $rolenames;
 338      }
 339  
 340      /**
 341       * Return array of all custom course fields indexed by their shortname
 342       *
 343       * @return \core_customfield\field_controller[]
 344       */
 345      public static function get_custom_course_fields(): array {
 346          $result = [];
 347  
 348          $fields = \core_course\customfield\course_handler::create()->get_fields();
 349          foreach ($fields as $field) {
 350              $result[$field->get('shortname')] = $field;
 351          }
 352  
 353          return $result;
 354      }
 355  
 356      /**
 357       * Return array of custom field element names
 358       *
 359       * @return string[]
 360       */
 361      public static function get_custom_course_field_names(): array {
 362          $result = [];
 363  
 364          $fields = self::get_custom_course_fields();
 365          foreach ($fields as $field) {
 366              $controller = \core_customfield\data_controller::create(0, null, $field);
 367              $result[] = $controller->get_form_element_name();
 368          }
 369  
 370          return $result;
 371      }
 372  
 373      /**
 374       * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
 375       *
 376       * @param array $data
 377       * @param array $defaults
 378       * @param context $context
 379       * @param array $errors Will be populated with any errors
 380       * @return array
 381       */
 382      public static function get_custom_course_field_data(array $data, array $defaults, context $context,
 383              array &$errors = []): array {
 384  
 385          $fields = self::get_custom_course_fields();
 386          $result = [];
 387  
 388          $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
 389  
 390          foreach ($data as $name => $originalvalue) {
 391              if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
 392                      && isset($fields[$matches['name']])) {
 393  
 394                  $fieldname = $matches['name'];
 395                  $field = $fields[$fieldname];
 396  
 397                  // Skip field if it's locked and user doesn't have capability to change locked fields.
 398                  if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
 399                      continue;
 400                  }
 401  
 402                  // Create field data controller.
 403                  $controller = \core_customfield\data_controller::create(0, null, $field);
 404                  $controller->set('id', 1);
 405  
 406                  $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
 407                  $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
 408  
 409                  // If we initially had a value, but now don't, then reset it to the default.
 410                  if (!empty($originalvalue) && empty($value)) {
 411                      $value = $defaultvalue;
 412                  }
 413  
 414                  // Validate data with controller.
 415                  $fieldformdata = [$controller->get_form_element_name() => $value];
 416                  $validationerrors = $controller->instance_form_validation($fieldformdata, []);
 417                  if (count($validationerrors) > 0) {
 418                      $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
 419                          $field->get_formatted_name());
 420  
 421                      continue;
 422                  }
 423  
 424                  $controller->set($controller->datafield(), $value);
 425  
 426                  // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
 427                  $instance = new stdClass();
 428                  $controller->instance_form_before_set_data($instance);
 429  
 430                  $result = array_merge($result, (array) $instance);
 431              }
 432          }
 433  
 434          return $result;
 435      }
 436  
 437      /**
 438       * Helper to increment an ID number.
 439       *
 440       * This first checks if the ID number is in use.
 441       *
 442       * @param string $idnumber ID number to increment.
 443       * @return string new ID number.
 444       */
 445      public static function increment_idnumber($idnumber) {
 446          global $DB;
 447          while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
 448              $matches = array();
 449              if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
 450                  $newidnumber = $idnumber . '_2';
 451              } else {
 452                  $newidnumber = $matches[1] . ((int) $matches[2] + 1);
 453              }
 454              $idnumber = $newidnumber;
 455          }
 456          return $idnumber;
 457      }
 458  
 459      /**
 460       * Helper to increment a shortname.
 461       *
 462       * This considers that the shortname passed has to be incremented.
 463       *
 464       * @param string $shortname shortname to increment.
 465       * @return string new shortname.
 466       */
 467      public static function increment_shortname($shortname) {
 468          global $DB;
 469          do {
 470              $matches = array();
 471              if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
 472                  $newshortname = $shortname . '_2';
 473              } else {
 474                  $newshortname = $matches[1] . ($matches[2]+1);
 475              }
 476              $shortname = $newshortname;
 477          } while ($DB->record_exists('course', array('shortname' => $shortname)));
 478          return $shortname;
 479      }
 480  
 481      /**
 482       * Resolve a category based on the data passed.
 483       *
 484       * Key accepted are:
 485       * - category, which is supposed to be a category ID.
 486       * - category_idnumber
 487       * - category_path, array of categories from parent to child.
 488       *
 489       * @param array $data to resolve the category from.
 490       * @param array $errors will be populated with errors found.
 491       * @return int category ID.
 492       */
 493      public static function resolve_category($data, &$errors = array()) {
 494          $catid = null;
 495  
 496          if (!empty($data['category'])) {
 497              $category = core_course_category::get((int) $data['category'], IGNORE_MISSING);
 498              if (!empty($category) && !empty($category->id)) {
 499                  $catid = $category->id;
 500              } else {
 501                  $errors['couldnotresolvecatgorybyid'] =
 502                      new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
 503              }
 504          }
 505  
 506          if (empty($catid) && !empty($data['category_idnumber'])) {
 507              $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
 508              if (empty($catid)) {
 509                  $errors['couldnotresolvecatgorybyidnumber'] =
 510                      new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
 511              }
 512          }
 513          if (empty($catid) && !empty($data['category_path'])) {
 514              $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
 515              if (empty($catid)) {
 516                  $errors['couldnotresolvecatgorybypath'] =
 517                      new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
 518              }
 519          }
 520  
 521          return $catid;
 522      }
 523  
 524      /**
 525       * Resolve a category by ID number.
 526       *
 527       * @param string $idnumber category ID number.
 528       * @return int category ID.
 529       */
 530      public static function resolve_category_by_idnumber($idnumber) {
 531          global $DB;
 532          $cache = cache::make('tool_uploadcourse', 'helper');
 533          $cachekey = 'cat_idn_' . $idnumber;
 534          if (($id = $cache->get($cachekey)) === false) {
 535              $params = array('idnumber' => $idnumber);
 536              $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
 537  
 538              // Little hack to be able to differenciate between the cache not set and a category not found.
 539              if ($id === false) {
 540                  $id = -1;
 541              }
 542  
 543              $cache->set($cachekey, $id);
 544          }
 545  
 546          // Little hack to be able to differenciate between the cache not set and a category not found.
 547          if ($id == -1) {
 548              $id = false;
 549          }
 550  
 551          return $id;
 552      }
 553  
 554      /**
 555       * Resolve a category by path.
 556       *
 557       * @param array $path category names indexed from parent to children.
 558       * @return int category ID.
 559       */
 560      public static function resolve_category_by_path(array $path) {
 561          global $DB;
 562          $cache = cache::make('tool_uploadcourse', 'helper');
 563          $cachekey = 'cat_path_' . serialize($path);
 564          if (($id = $cache->get($cachekey)) === false) {
 565              $parent = 0;
 566              $sql = 'name = :name AND parent = :parent';
 567              while ($name = array_shift($path)) {
 568                  $params = array('name' => $name, 'parent' => $parent);
 569                  if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
 570                      if (count($records) > 1) {
 571                          // Too many records with the same name!
 572                          $id = -1;
 573                          break;
 574                      }
 575                      $record = reset($records);
 576                      $id = $record->id;
 577                      $parent = $record->id;
 578                  } else {
 579                      // Not found.
 580                      $id = -1;
 581                      break;
 582                  }
 583              }
 584              $cache->set($cachekey, $id);
 585          }
 586  
 587          // We save -1 when the category has not been found to be able to know if the cache was set.
 588          if ($id == -1) {
 589              $id = false;
 590          }
 591          return $id;
 592      }
 593  }