Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 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 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  }