Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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 enrol_plugin[]
 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              } else if (preg_match('/^(.+)?_role$/', $field, $matches)) {
 329                  if (!isset($rolesids[$value])) {
 330                      $invalidroles[] = $value;
 331                      break;
 332                  }
 333              }
 334  
 335          }
 336  
 337          if (!empty($invalidroles)) {
 338              $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
 339          }
 340  
 341          // Roles names.
 342          return $rolenames;
 343      }
 344  
 345      /**
 346       * Return array of all custom course fields indexed by their shortname
 347       *
 348       * @return \core_customfield\field_controller[]
 349       */
 350      public static function get_custom_course_fields(): array {
 351          $result = [];
 352  
 353          $fields = \core_course\customfield\course_handler::create()->get_fields();
 354          foreach ($fields as $field) {
 355              $result[$field->get('shortname')] = $field;
 356          }
 357  
 358          return $result;
 359      }
 360  
 361      /**
 362       * Return array of custom field element names
 363       *
 364       * @return string[]
 365       */
 366      public static function get_custom_course_field_names(): array {
 367          $result = [];
 368  
 369          $fields = self::get_custom_course_fields();
 370          foreach ($fields as $field) {
 371              $controller = \core_customfield\data_controller::create(0, null, $field);
 372              $result[] = $controller->get_form_element_name();
 373          }
 374  
 375          return $result;
 376      }
 377  
 378      /**
 379       * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
 380       *
 381       * @param array $data
 382       * @param array $defaults
 383       * @param context $context
 384       * @param array $errors Will be populated with any errors
 385       * @return array
 386       */
 387      public static function get_custom_course_field_data(array $data, array $defaults, context $context,
 388              array &$errors = []): array {
 389  
 390          $fields = self::get_custom_course_fields();
 391          $result = [];
 392  
 393          $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
 394  
 395          foreach ($data as $name => $originalvalue) {
 396              if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
 397                      && isset($fields[$matches['name']])) {
 398  
 399                  $fieldname = $matches['name'];
 400                  $field = $fields[$fieldname];
 401  
 402                  // Skip field if it's locked and user doesn't have capability to change locked fields.
 403                  if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
 404                      continue;
 405                  }
 406  
 407                  // Create field data controller.
 408                  $controller = \core_customfield\data_controller::create(0, null, $field);
 409                  $controller->set('id', 1);
 410  
 411                  $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
 412                  $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
 413  
 414                  // If we initially had a value, but now don't, then reset it to the default.
 415                  if (!empty($originalvalue) && empty($value)) {
 416                      $value = $defaultvalue;
 417                  }
 418  
 419                  // Validate data with controller.
 420                  $fieldformdata = [$controller->get_form_element_name() => $value];
 421                  $validationerrors = $controller->instance_form_validation($fieldformdata, []);
 422                  if (count($validationerrors) > 0) {
 423                      $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
 424                          $field->get_formatted_name());
 425  
 426                      continue;
 427                  }
 428  
 429                  $controller->set($controller->datafield(), $value);
 430  
 431                  // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
 432                  $instance = new stdClass();
 433                  $controller->instance_form_before_set_data($instance);
 434  
 435                  $result = array_merge($result, (array) $instance);
 436              }
 437          }
 438  
 439          return $result;
 440      }
 441  
 442      /**
 443       * Helper to increment an ID number.
 444       *
 445       * This first checks if the ID number is in use.
 446       *
 447       * @param string $idnumber ID number to increment.
 448       * @return string new ID number.
 449       */
 450      public static function increment_idnumber($idnumber) {
 451          global $DB;
 452          while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
 453              $matches = array();
 454              if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
 455                  $newidnumber = $idnumber . '_2';
 456              } else {
 457                  $newidnumber = $matches[1] . ((int) $matches[2] + 1);
 458              }
 459              $idnumber = $newidnumber;
 460          }
 461          return $idnumber;
 462      }
 463  
 464      /**
 465       * Helper to increment a shortname.
 466       *
 467       * This considers that the shortname passed has to be incremented.
 468       *
 469       * @param string $shortname shortname to increment.
 470       * @return string new shortname.
 471       */
 472      public static function increment_shortname($shortname) {
 473          global $DB;
 474          do {
 475              $matches = array();
 476              if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
 477                  $newshortname = $shortname . '_2';
 478              } else {
 479                  $newshortname = $matches[1] . ($matches[2]+1);
 480              }
 481              $shortname = $newshortname;
 482          } while ($DB->record_exists('course', array('shortname' => $shortname)));
 483          return $shortname;
 484      }
 485  
 486      /**
 487       * Resolve a category based on the data passed.
 488       *
 489       * Key accepted are:
 490       * - category, which is supposed to be a category ID.
 491       * - category_idnumber
 492       * - category_path, array of categories from parent to child.
 493       *
 494       * @param array $data to resolve the category from.
 495       * @param array $errors will be populated with errors found.
 496       * @return int category ID.
 497       */
 498      public static function resolve_category($data, &$errors = array()) {
 499          $catid = null;
 500  
 501          if (!empty($data['category'])) {
 502              $category = core_course_category::get((int) $data['category'], IGNORE_MISSING);
 503              if (!empty($category) && !empty($category->id)) {
 504                  $catid = $category->id;
 505              } else {
 506                  $errors['couldnotresolvecatgorybyid'] =
 507                      new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
 508              }
 509          }
 510  
 511          if (empty($catid) && !empty($data['category_idnumber'])) {
 512              $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
 513              if (empty($catid)) {
 514                  $errors['couldnotresolvecatgorybyidnumber'] =
 515                      new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
 516              }
 517          }
 518          if (empty($catid) && !empty($data['category_path'])) {
 519              $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
 520              if (empty($catid)) {
 521                  $errors['couldnotresolvecatgorybypath'] =
 522                      new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
 523              }
 524          }
 525  
 526          return $catid;
 527      }
 528  
 529      /**
 530       * Resolve a category by ID number.
 531       *
 532       * @param string $idnumber category ID number.
 533       * @return int category ID.
 534       */
 535      public static function resolve_category_by_idnumber($idnumber) {
 536          global $DB;
 537          $cache = cache::make('tool_uploadcourse', 'helper');
 538          $cachekey = 'cat_idn_' . $idnumber;
 539          if (($id = $cache->get($cachekey)) === false) {
 540              $params = array('idnumber' => $idnumber);
 541              $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
 542  
 543              // Little hack to be able to differenciate between the cache not set and a category not found.
 544              if ($id === false) {
 545                  $id = -1;
 546              }
 547  
 548              $cache->set($cachekey, $id);
 549          }
 550  
 551          // Little hack to be able to differenciate between the cache not set and a category not found.
 552          if ($id == -1) {
 553              $id = false;
 554          }
 555  
 556          return $id;
 557      }
 558  
 559      /**
 560       * Resolve a category by path.
 561       *
 562       * @param array $path category names indexed from parent to children.
 563       * @return int category ID.
 564       */
 565      public static function resolve_category_by_path(array $path) {
 566          global $DB;
 567          $cache = cache::make('tool_uploadcourse', 'helper');
 568          $cachekey = 'cat_path_' . serialize($path);
 569          if (($id = $cache->get($cachekey)) === false) {
 570              $parent = 0;
 571              $sql = 'name = :name AND parent = :parent';
 572              while ($name = array_shift($path)) {
 573                  $params = array('name' => $name, 'parent' => $parent);
 574                  if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
 575                      if (count($records) > 1) {
 576                          // Too many records with the same name!
 577                          $id = -1;
 578                          break;
 579                      }
 580                      $record = reset($records);
 581                      $id = $record->id;
 582                      $parent = $record->id;
 583                  } else {
 584                      // Not found.
 585                      $id = -1;
 586                      break;
 587                  }
 588              }
 589              $cache->set($cachekey, $id);
 590          }
 591  
 592          // We save -1 when the category has not been found to be able to know if the cache was set.
 593          if ($id == -1) {
 594              $id = false;
 595          }
 596          return $id;
 597      }
 598  }