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 311 and 400] [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   * Contains class core_course_category responsible for course category operations
      19   *
      20   * @package    core
      21   * @subpackage course
      22   * @copyright  2013 Marina Glancy
      23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      24   */
      25  
      26  defined('MOODLE_INTERNAL') || die();
      27  
      28  /**
      29   * Class to store, cache, render and manage course category
      30   *
      31   * @property-read int $id
      32   * @property-read string $name
      33   * @property-read string $idnumber
      34   * @property-read string $description
      35   * @property-read int $descriptionformat
      36   * @property-read int $parent
      37   * @property-read int $sortorder
      38   * @property-read int $coursecount
      39   * @property-read int $visible
      40   * @property-read int $visibleold
      41   * @property-read int $timemodified
      42   * @property-read int $depth
      43   * @property-read string $path
      44   * @property-read string $theme
      45   *
      46   * @package    core
      47   * @subpackage course
      48   * @copyright  2013 Marina Glancy
      49   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      50   */
      51  class core_course_category implements renderable, cacheable_object, IteratorAggregate {
      52      /** @var core_course_category stores pseudo category with id=0. Use core_course_category::get(0) to retrieve */
      53      protected static $coursecat0;
      54  
      55      /** @var array list of all fields and their short name and default value for caching */
      56      protected static $coursecatfields = array(
      57          'id' => array('id', 0),
      58          'name' => array('na', ''),
      59          'idnumber' => array('in', null),
      60          'description' => null, // Not cached.
      61          'descriptionformat' => null, // Not cached.
      62          'parent' => array('pa', 0),
      63          'sortorder' => array('so', 0),
      64          'coursecount' => array('cc', 0),
      65          'visible' => array('vi', 1),
      66          'visibleold' => null, // Not cached.
      67          'timemodified' => null, // Not cached.
      68          'depth' => array('dh', 1),
      69          'path' => array('ph', null),
      70          'theme' => null, // Not cached.
      71      );
      72  
      73      /** @var int */
      74      protected $id;
      75  
      76      /** @var string */
      77      protected $name = '';
      78  
      79      /** @var string */
      80      protected $idnumber = null;
      81  
      82      /** @var string */
      83      protected $description = false;
      84  
      85      /** @var int */
      86      protected $descriptionformat = false;
      87  
      88      /** @var int */
      89      protected $parent = 0;
      90  
      91      /** @var int */
      92      protected $sortorder = 0;
      93  
      94      /** @var int */
      95      protected $coursecount = false;
      96  
      97      /** @var int */
      98      protected $visible = 1;
      99  
     100      /** @var int */
     101      protected $visibleold = false;
     102  
     103      /** @var int */
     104      protected $timemodified = false;
     105  
     106      /** @var int */
     107      protected $depth = 0;
     108  
     109      /** @var string */
     110      protected $path = '';
     111  
     112      /** @var string */
     113      protected $theme = false;
     114  
     115      /** @var bool */
     116      protected $fromcache = false;
     117  
     118      /**
     119       * Magic setter method, we do not want anybody to modify properties from the outside
     120       *
     121       * @param string $name
     122       * @param mixed $value
     123       */
     124      public function __set($name, $value) {
     125          debugging('Can not change core_course_category instance properties!', DEBUG_DEVELOPER);
     126      }
     127  
     128      /**
     129       * Magic method getter, redirects to read only values. Queries from DB the fields that were not cached
     130       *
     131       * @param string $name
     132       * @return mixed
     133       */
     134      public function __get($name) {
     135          global $DB;
     136          if (array_key_exists($name, self::$coursecatfields)) {
     137              if ($this->$name === false) {
     138                  // Property was not retrieved from DB, retrieve all not retrieved fields.
     139                  $notretrievedfields = array_diff_key(self::$coursecatfields, array_filter(self::$coursecatfields));
     140                  $record = $DB->get_record('course_categories', array('id' => $this->id),
     141                          join(',', array_keys($notretrievedfields)), MUST_EXIST);
     142                  foreach ($record as $key => $value) {
     143                      $this->$key = $value;
     144                  }
     145              }
     146              return $this->$name;
     147          }
     148          debugging('Invalid core_course_category property accessed! '.$name, DEBUG_DEVELOPER);
     149          return null;
     150      }
     151  
     152      /**
     153       * Full support for isset on our magic read only properties.
     154       *
     155       * @param string $name
     156       * @return bool
     157       */
     158      public function __isset($name) {
     159          if (array_key_exists($name, self::$coursecatfields)) {
     160              return isset($this->$name);
     161          }
     162          return false;
     163      }
     164  
     165      /**
     166       * All properties are read only, sorry.
     167       *
     168       * @param string $name
     169       */
     170      public function __unset($name) {
     171          debugging('Can not unset core_course_category instance properties!', DEBUG_DEVELOPER);
     172      }
     173  
     174      /**
     175       * Get list of plugin callback functions.
     176       *
     177       * @param string $name Callback function name.
     178       * @return [callable] $pluginfunctions
     179       */
     180      public function get_plugins_callback_function(string $name) : array {
     181          $pluginfunctions = [];
     182          if ($pluginsfunction = get_plugins_with_function($name)) {
     183              foreach ($pluginsfunction as $plugintype => $plugins) {
     184                  foreach ($plugins as $pluginfunction) {
     185                      $pluginfunctions[] = $pluginfunction;
     186                  }
     187              }
     188          }
     189          return $pluginfunctions;
     190      }
     191  
     192      /**
     193       * Create an iterator because magic vars can't be seen by 'foreach'.
     194       *
     195       * implementing method from interface IteratorAggregate
     196       *
     197       * @return ArrayIterator
     198       */
     199      public function getIterator() {
     200          $ret = array();
     201          foreach (self::$coursecatfields as $property => $unused) {
     202              if ($this->$property !== false) {
     203                  $ret[$property] = $this->$property;
     204              }
     205          }
     206          return new ArrayIterator($ret);
     207      }
     208  
     209      /**
     210       * Constructor
     211       *
     212       * Constructor is protected, use core_course_category::get($id) to retrieve category
     213       *
     214       * @param stdClass $record record from DB (may not contain all fields)
     215       * @param bool $fromcache whether it is being restored from cache
     216       */
     217      protected function __construct(stdClass $record, $fromcache = false) {
     218          context_helper::preload_from_record($record);
     219          foreach ($record as $key => $val) {
     220              if (array_key_exists($key, self::$coursecatfields)) {
     221                  $this->$key = $val;
     222              }
     223          }
     224          $this->fromcache = $fromcache;
     225      }
     226  
     227      /**
     228       * Returns coursecat object for requested category
     229       *
     230       * If category is not visible to the given user, it is treated as non existing
     231       * unless $alwaysreturnhidden is set to true
     232       *
     233       * If id is 0, the pseudo object for root category is returned (convenient
     234       * for calling other functions such as get_children())
     235       *
     236       * @param int $id category id
     237       * @param int $strictness whether to throw an exception (MUST_EXIST) or
     238       *     return null (IGNORE_MISSING) in case the category is not found or
     239       *     not visible to current user
     240       * @param bool $alwaysreturnhidden set to true if you want an object to be
     241       *     returned even if this category is not visible to the current user
     242       *     (category is hidden and user does not have
     243       *     'moodle/category:viewhiddencategories' capability). Use with care!
     244       * @param int|stdClass $user The user id or object. By default (null) checks the visibility to the current user.
     245       * @return null|self
     246       * @throws moodle_exception
     247       */
     248      public static function get($id, $strictness = MUST_EXIST, $alwaysreturnhidden = false, $user = null) {
     249          if (!$id) {
     250              // Top-level category.
     251              if ($alwaysreturnhidden || self::top()->is_uservisible()) {
     252                  return self::top();
     253              }
     254              if ($strictness == MUST_EXIST) {
     255                  throw new moodle_exception('cannotviewcategory');
     256              }
     257              return null;
     258          }
     259  
     260          // Try to get category from cache or retrieve from the DB.
     261          $coursecatrecordcache = cache::make('core', 'coursecatrecords');
     262          $coursecat = $coursecatrecordcache->get($id);
     263          if ($coursecat === false) {
     264              if ($records = self::get_records('cc.id = :id', array('id' => $id))) {
     265                  $record = reset($records);
     266                  $coursecat = new self($record);
     267                  // Store in cache.
     268                  $coursecatrecordcache->set($id, $coursecat);
     269              }
     270          }
     271  
     272          if (!$coursecat) {
     273              // Course category not found.
     274              if ($strictness == MUST_EXIST) {
     275                  throw new moodle_exception('unknowncategory');
     276              }
     277              $coursecat = null;
     278          } else if (!$alwaysreturnhidden && !$coursecat->is_uservisible($user)) {
     279              // Course category is found but user can not access it.
     280              if ($strictness == MUST_EXIST) {
     281                  throw new moodle_exception('cannotviewcategory');
     282              }
     283              $coursecat = null;
     284          }
     285          return $coursecat;
     286      }
     287  
     288      /**
     289       * Returns the pseudo-category representing the whole system (id=0, context_system)
     290       *
     291       * @return core_course_category
     292       */
     293      public static function top() {
     294          if (!isset(self::$coursecat0)) {
     295              $record = new stdClass();
     296              $record->id = 0;
     297              $record->visible = 1;
     298              $record->depth = 0;
     299              $record->path = '';
     300              $record->locked = 0;
     301              self::$coursecat0 = new self($record);
     302          }
     303          return self::$coursecat0;
     304      }
     305  
     306      /**
     307       * Returns the top-most category for the current user
     308       *
     309       * Examples:
     310       * 1. User can browse courses everywhere - return self::top() - pseudo-category with id=0
     311       * 2. User does not have capability to browse courses on the system level but
     312       *    has it in ONE course category - return this course category
     313       * 3. User has capability to browse courses in two course categories - return self::top()
     314       *
     315       * @return core_course_category|null
     316       */
     317      public static function user_top() {
     318          $children = self::top()->get_children();
     319          if (count($children) == 1) {
     320              // User has access to only one category on the top level. Return this category as "user top category".
     321              return reset($children);
     322          }
     323          if (count($children) > 1) {
     324              // User has access to more than one category on the top level. Return the top as "user top category".
     325              // In this case user actually may not have capability 'moodle/category:viewcourselist' on the top level.
     326              return self::top();
     327          }
     328          // User can not access any categories on the top level.
     329          // TODO MDL-10965 find ANY/ALL categories in the tree where user has access to.
     330          return self::get(0, IGNORE_MISSING);
     331      }
     332  
     333      /**
     334       * Load many core_course_category objects.
     335       *
     336       * @param array $ids An array of category ID's to load.
     337       * @return core_course_category[]
     338       */
     339      public static function get_many(array $ids) {
     340          global $DB;
     341          $coursecatrecordcache = cache::make('core', 'coursecatrecords');
     342          $categories = $coursecatrecordcache->get_many($ids);
     343          $toload = array();
     344          foreach ($categories as $id => $result) {
     345              if ($result === false) {
     346                  $toload[] = $id;
     347              }
     348          }
     349          if (!empty($toload)) {
     350              list($where, $params) = $DB->get_in_or_equal($toload, SQL_PARAMS_NAMED);
     351              $records = self::get_records('cc.id '.$where, $params);
     352              $toset = array();
     353              foreach ($records as $record) {
     354                  $categories[$record->id] = new self($record);
     355                  $toset[$record->id] = $categories[$record->id];
     356              }
     357              $coursecatrecordcache->set_many($toset);
     358          }
     359          return $categories;
     360      }
     361  
     362      /**
     363       * Load all core_course_category objects.
     364       *
     365       * @param array $options Options:
     366       *              - returnhidden Return categories even if they are hidden
     367       * @return  core_course_category[]
     368       */
     369      public static function get_all($options = []) {
     370          global $DB;
     371  
     372          $coursecatrecordcache = cache::make('core', 'coursecatrecords');
     373  
     374          $catcontextsql = \context_helper::get_preload_record_columns_sql('ctx');
     375          $catsql = "SELECT cc.*, {$catcontextsql}
     376                       FROM {course_categories} cc
     377                       JOIN {context} ctx ON cc.id = ctx.instanceid";
     378          $catsqlwhere = "WHERE ctx.contextlevel = :contextlevel";
     379          $catsqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC";
     380  
     381          $catrs = $DB->get_recordset_sql("{$catsql} {$catsqlwhere} {$catsqlorder}", [
     382              'contextlevel' => CONTEXT_COURSECAT,
     383          ]);
     384  
     385          $types['categories'] = [];
     386          $categories = [];
     387          $toset = [];
     388          foreach ($catrs as $record) {
     389              $category = new self($record);
     390              $toset[$category->id] = $category;
     391  
     392              if (!empty($options['returnhidden']) || $category->is_uservisible()) {
     393                  $categories[$record->id] = $category;
     394              }
     395          }
     396          $catrs->close();
     397  
     398          $coursecatrecordcache->set_many($toset);
     399  
     400          return $categories;
     401  
     402      }
     403  
     404      /**
     405       * Returns the first found category
     406       *
     407       * Note that if there are no categories visible to the current user on the first level,
     408       * the invisible category may be returned
     409       *
     410       * @return core_course_category
     411       */
     412      public static function get_default() {
     413          if ($visiblechildren = self::top()->get_children()) {
     414              $defcategory = reset($visiblechildren);
     415          } else {
     416              $toplevelcategories = self::get_tree(0);
     417              $defcategoryid = $toplevelcategories[0];
     418              $defcategory = self::get($defcategoryid, MUST_EXIST, true);
     419          }
     420          return $defcategory;
     421      }
     422  
     423      /**
     424       * Restores the object after it has been externally modified in DB for example
     425       * during {@link fix_course_sortorder()}
     426       */
     427      protected function restore() {
     428          if (!$this->id) {
     429              return;
     430          }
     431          // Update all fields in the current object.
     432          $newrecord = self::get($this->id, MUST_EXIST, true);
     433          foreach (self::$coursecatfields as $key => $unused) {
     434              $this->$key = $newrecord->$key;
     435          }
     436      }
     437  
     438      /**
     439       * Creates a new category either from form data or from raw data
     440       *
     441       * Please note that this function does not verify access control.
     442       *
     443       * Exception is thrown if name is missing or idnumber is duplicating another one in the system.
     444       *
     445       * Category visibility is inherited from parent unless $data->visible = 0 is specified
     446       *
     447       * @param array|stdClass $data
     448       * @param array $editoroptions if specified, the data is considered to be
     449       *    form data and file_postupdate_standard_editor() is being called to
     450       *    process images in description.
     451       * @return core_course_category
     452       * @throws moodle_exception
     453       */
     454      public static function create($data, $editoroptions = null) {
     455          global $DB, $CFG;
     456          $data = (object)$data;
     457          $newcategory = new stdClass();
     458  
     459          $newcategory->descriptionformat = FORMAT_MOODLE;
     460          $newcategory->description = '';
     461          // Copy all description* fields regardless of whether this is form data or direct field update.
     462          foreach ($data as $key => $value) {
     463              if (preg_match("/^description/", $key)) {
     464                  $newcategory->$key = $value;
     465              }
     466          }
     467  
     468          if (empty($data->name)) {
     469              throw new moodle_exception('categorynamerequired');
     470          }
     471          if (core_text::strlen($data->name) > 255) {
     472              throw new moodle_exception('categorytoolong');
     473          }
     474          $newcategory->name = $data->name;
     475  
     476          // Validate and set idnumber.
     477          if (isset($data->idnumber)) {
     478              if (core_text::strlen($data->idnumber) > 100) {
     479                  throw new moodle_exception('idnumbertoolong');
     480              }
     481              if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
     482                  throw new moodle_exception('categoryidnumbertaken');
     483              }
     484              $newcategory->idnumber = $data->idnumber;
     485          }
     486  
     487          if (isset($data->theme) && !empty($CFG->allowcategorythemes)) {
     488              $newcategory->theme = $data->theme;
     489          }
     490  
     491          if (empty($data->parent)) {
     492              $parent = self::top();
     493          } else {
     494              $parent = self::get($data->parent, MUST_EXIST, true);
     495          }
     496          $newcategory->parent = $parent->id;
     497          $newcategory->depth = $parent->depth + 1;
     498  
     499          // By default category is visible, unless visible = 0 is specified or parent category is hidden.
     500          if (isset($data->visible) && !$data->visible) {
     501              // Create a hidden category.
     502              $newcategory->visible = $newcategory->visibleold = 0;
     503          } else {
     504              // Create a category that inherits visibility from parent.
     505              $newcategory->visible = $parent->visible;
     506              // In case parent is hidden, when it changes visibility this new subcategory will automatically become visible too.
     507              $newcategory->visibleold = 1;
     508          }
     509  
     510          $newcategory->sortorder = 0;
     511          $newcategory->timemodified = time();
     512  
     513          $newcategory->id = $DB->insert_record('course_categories', $newcategory);
     514  
     515          // Update path (only possible after we know the category id.
     516          $path = $parent->path . '/' . $newcategory->id;
     517          $DB->set_field('course_categories', 'path', $path, array('id' => $newcategory->id));
     518  
     519          fix_course_sortorder();
     520  
     521          // If this is data from form results, save embedded files and update description.
     522          $categorycontext = context_coursecat::instance($newcategory->id);
     523          if ($editoroptions) {
     524              $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext,
     525                                                             'coursecat', 'description', 0);
     526  
     527              // Update only fields description and descriptionformat.
     528              $updatedata = new stdClass();
     529              $updatedata->id = $newcategory->id;
     530              $updatedata->description = $newcategory->description;
     531              $updatedata->descriptionformat = $newcategory->descriptionformat;
     532              $DB->update_record('course_categories', $updatedata);
     533          }
     534  
     535          $event = \core\event\course_category_created::create(array(
     536              'objectid' => $newcategory->id,
     537              'context' => $categorycontext
     538          ));
     539          $event->trigger();
     540  
     541          cache_helper::purge_by_event('changesincoursecat');
     542  
     543          return self::get($newcategory->id, MUST_EXIST, true);
     544      }
     545  
     546      /**
     547       * Updates the record with either form data or raw data
     548       *
     549       * Please note that this function does not verify access control.
     550       *
     551       * This function calls core_course_category::change_parent_raw if field 'parent' is updated.
     552       * It also calls core_course_category::hide_raw or core_course_category::show_raw if 'visible' is updated.
     553       * Visibility is changed first and then parent is changed. This means that
     554       * if parent category is hidden, the current category will become hidden
     555       * too and it may overwrite whatever was set in field 'visible'.
     556       *
     557       * Note that fields 'path' and 'depth' can not be updated manually
     558       * Also core_course_category::update() can not directly update the field 'sortoder'
     559       *
     560       * @param array|stdClass $data
     561       * @param array $editoroptions if specified, the data is considered to be
     562       *    form data and file_postupdate_standard_editor() is being called to
     563       *    process images in description.
     564       * @throws moodle_exception
     565       */
     566      public function update($data, $editoroptions = null) {
     567          global $DB, $CFG;
     568          if (!$this->id) {
     569              // There is no actual DB record associated with root category.
     570              return;
     571          }
     572  
     573          $data = (object)$data;
     574          $newcategory = new stdClass();
     575          $newcategory->id = $this->id;
     576  
     577          // Copy all description* fields regardless of whether this is form data or direct field update.
     578          foreach ($data as $key => $value) {
     579              if (preg_match("/^description/", $key)) {
     580                  $newcategory->$key = $value;
     581              }
     582          }
     583  
     584          if (isset($data->name) && empty($data->name)) {
     585              throw new moodle_exception('categorynamerequired');
     586          }
     587  
     588          if (!empty($data->name) && $data->name !== $this->name) {
     589              if (core_text::strlen($data->name) > 255) {
     590                  throw new moodle_exception('categorytoolong');
     591              }
     592              $newcategory->name = $data->name;
     593          }
     594  
     595          if (isset($data->idnumber) && $data->idnumber !== $this->idnumber) {
     596              if (core_text::strlen($data->idnumber) > 100) {
     597                  throw new moodle_exception('idnumbertoolong');
     598              }
     599              if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
     600                  throw new moodle_exception('categoryidnumbertaken');
     601              }
     602              $newcategory->idnumber = $data->idnumber;
     603          }
     604  
     605          if (isset($data->theme) && !empty($CFG->allowcategorythemes)) {
     606              $newcategory->theme = $data->theme;
     607          }
     608  
     609          $changes = false;
     610          if (isset($data->visible)) {
     611              if ($data->visible) {
     612                  $changes = $this->show_raw();
     613              } else {
     614                  $changes = $this->hide_raw(0);
     615              }
     616          }
     617  
     618          if (isset($data->parent) && $data->parent != $this->parent) {
     619              if ($changes) {
     620                  cache_helper::purge_by_event('changesincoursecat');
     621              }
     622              $parentcat = self::get($data->parent, MUST_EXIST, true);
     623              $this->change_parent_raw($parentcat);
     624              fix_course_sortorder();
     625          }
     626  
     627          $newcategory->timemodified = time();
     628  
     629          $categorycontext = $this->get_context();
     630          if ($editoroptions) {
     631              $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext,
     632                                                             'coursecat', 'description', 0);
     633          }
     634          $DB->update_record('course_categories', $newcategory);
     635  
     636          $event = \core\event\course_category_updated::create(array(
     637              'objectid' => $newcategory->id,
     638              'context' => $categorycontext
     639          ));
     640          $event->trigger();
     641  
     642          fix_course_sortorder();
     643          // Purge cache even if fix_course_sortorder() did not do it.
     644          cache_helper::purge_by_event('changesincoursecat');
     645  
     646          // Update all fields in the current object.
     647          $this->restore();
     648      }
     649  
     650  
     651      /**
     652       * Checks if this course category is visible to a user.
     653       *
     654       * Please note that methods core_course_category::get (without 3rd argumet),
     655       * core_course_category::get_children(), etc. return only visible categories so it is
     656       * usually not needed to call this function outside of this class
     657       *
     658       * @param int|stdClass $user The user id or object. By default (null) checks the visibility to the current user.
     659       * @return bool
     660       */
     661      public function is_uservisible($user = null) {
     662          return self::can_view_category($this, $user);
     663      }
     664  
     665      /**
     666       * Checks if current user has access to the category
     667       *
     668       * @param stdClass|core_course_category $category
     669       * @param int|stdClass $user The user id or object. By default (null) checks access for the current user.
     670       * @return bool
     671       */
     672      public static function can_view_category($category, $user = null) {
     673          if (!$category->id) {
     674              return has_capability('moodle/category:viewcourselist', context_system::instance(), $user);
     675          }
     676          $context = context_coursecat::instance($category->id);
     677          if (!$category->visible && !has_capability('moodle/category:viewhiddencategories', $context, $user)) {
     678              return false;
     679          }
     680          return has_capability('moodle/category:viewcourselist', $context, $user);
     681      }
     682  
     683      /**
     684       * Checks if current user can view course information or enrolment page.
     685       *
     686       * This method does not check if user is already enrolled in the course
     687       *
     688       * @param stdClass $course course object (must have 'id', 'visible' and 'category' fields)
     689       * @param null|stdClass $user The user id or object. By default (null) checks access for the current user.
     690       */
     691      public static function can_view_course_info($course, $user = null) {
     692          if ($course->id == SITEID) {
     693              return true;
     694          }
     695          if (!$course->visible) {
     696              $coursecontext = context_course::instance($course->id);
     697              if (!has_capability('moodle/course:viewhiddencourses', $coursecontext, $user)) {
     698                  return false;
     699              }
     700          }
     701          $categorycontext = isset($course->category) ? context_coursecat::instance($course->category) :
     702              context_course::instance($course->id)->get_parent_context();
     703          return has_capability('moodle/category:viewcourselist', $categorycontext, $user);
     704      }
     705  
     706      /**
     707       * Returns the complete corresponding record from DB table course_categories
     708       *
     709       * Mostly used in deprecated functions
     710       *
     711       * @return stdClass
     712       */
     713      public function get_db_record() {
     714          global $DB;
     715          if ($record = $DB->get_record('course_categories', array('id' => $this->id))) {
     716              return $record;
     717          } else {
     718              return (object)convert_to_array($this);
     719          }
     720      }
     721  
     722      /**
     723       * Returns the entry from categories tree and makes sure the application-level tree cache is built
     724       *
     725       * The following keys can be requested:
     726       *
     727       * 'countall' - total number of categories in the system (always present)
     728       * 0 - array of ids of top-level categories (always present)
     729       * '0i' - array of ids of top-level categories that have visible=0 (always present but may be empty array)
     730       * $id (int) - array of ids of categories that are direct children of category with id $id. If
     731       *   category with id $id does not exist, or category has no children, returns empty array
     732       * $id.'i' - array of ids of children categories that have visible=0
     733       *
     734       * @param int|string $id
     735       * @return mixed
     736       */
     737      protected static function get_tree($id) {
     738          $all = self::get_cached_cat_tree();
     739          if (is_null($all) || !isset($all[$id])) {
     740              // Could not get or rebuild the tree, or requested a non-existant ID.
     741              return [];
     742          } else {
     743              return $all[$id];
     744          }
     745      }
     746  
     747      /**
     748       * Return the course category tree.
     749       *
     750       * Returns the category tree array, from the cache if available or rebuilding the cache
     751       * if required. Uses locking to prevent the cache being rebuilt by multiple requests at once.
     752       *
     753       * @return array|null The tree as an array, or null if rebuilding the tree failed due to a lock timeout.
     754       * @throws coding_exception
     755       * @throws dml_exception
     756       * @throws moodle_exception
     757       */
     758      private static function get_cached_cat_tree() : ?array {
     759          $coursecattreecache = cache::make('core', 'coursecattree');
     760          $all = $coursecattreecache->get('all');
     761          if ($all !== false) {
     762              return $all;
     763          }
     764          // Might need to rebuild the tree. Put a lock in place to ensure other requests don't try and do this in parallel.
     765          $lockfactory = \core\lock\lock_config::get_lock_factory('core_coursecattree');
     766          $lock = $lockfactory->get_lock('core_coursecattree_cache',
     767                  course_modinfo::COURSE_CACHE_LOCK_WAIT, course_modinfo::COURSE_CACHE_LOCK_EXPIRY);
     768          if ($lock === false) {
     769              // Couldn't get a lock to rebuild the tree.
     770              return null;
     771          }
     772          $all = $coursecattreecache->get('all');
     773          if ($all !== false) {
     774              // Tree was built while we were waiting for the lock.
     775              $lock->release();
     776              return $all;
     777          }
     778          // Re-build the tree.
     779          try {
     780              $all = self::rebuild_coursecattree_cache_contents();
     781              $coursecattreecache->set('all', $all);
     782          } finally {
     783              $lock->release();
     784          }
     785          return $all;
     786      }
     787  
     788      /**
     789       * Rebuild the course category tree as an array, including an extra "countall" field.
     790       *
     791       * @return array
     792       * @throws coding_exception
     793       * @throws dml_exception
     794       * @throws moodle_exception
     795       */
     796      private static function rebuild_coursecattree_cache_contents() : array {
     797          global $DB;
     798          $sql = "SELECT cc.id, cc.parent, cc.visible
     799                  FROM {course_categories} cc
     800                  ORDER BY cc.sortorder";
     801          $rs = $DB->get_recordset_sql($sql, array());
     802          $all = array(0 => array(), '0i' => array());
     803          $count = 0;
     804          foreach ($rs as $record) {
     805              $all[$record->id] = array();
     806              $all[$record->id. 'i'] = array();
     807              if (array_key_exists($record->parent, $all)) {
     808                  $all[$record->parent][] = $record->id;
     809                  if (!$record->visible) {
     810                      $all[$record->parent. 'i'][] = $record->id;
     811                  }
     812              } else {
     813                  // Parent not found. This is data consistency error but next fix_course_sortorder() should fix it.
     814                  $all[0][] = $record->id;
     815                  if (!$record->visible) {
     816                      $all['0i'][] = $record->id;
     817                  }
     818              }
     819              $count++;
     820          }
     821          $rs->close();
     822          if (!$count) {
     823              // No categories found.
     824              // This may happen after upgrade of a very old moodle version.
     825              // In new versions the default category is created on install.
     826              $defcoursecat = self::create(array('name' => get_string('miscellaneous')));
     827              set_config('defaultrequestcategory', $defcoursecat->id);
     828              $all[0] = array($defcoursecat->id);
     829              $all[$defcoursecat->id] = array();
     830              $count++;
     831          }
     832          // We must add countall to all in case it was the requested ID.
     833          $all['countall'] = $count;
     834          return $all;
     835      }
     836  
     837      /**
     838       * Returns number of ALL categories in the system regardless if
     839       * they are visible to current user or not
     840       *
     841       * @deprecated since Moodle 3.7
     842       * @return int
     843       */
     844      public static function count_all() {
     845          debugging('Method core_course_category::count_all() is deprecated. Please use ' .
     846              'core_course_category::is_simple_site()', DEBUG_DEVELOPER);
     847          return self::get_tree('countall');
     848      }
     849  
     850      /**
     851       * Checks if the site has only one category and it is visible and available.
     852       *
     853       * In many situations we won't show this category at all
     854       * @return bool
     855       */
     856      public static function is_simple_site() {
     857          if (self::get_tree('countall') != 1) {
     858              return false;
     859          }
     860          $default = self::get_default();
     861          return $default->visible && $default->is_uservisible();
     862      }
     863  
     864      /**
     865       * Retrieves number of records from course_categories table
     866       *
     867       * Only cached fields are retrieved. Records are ready for preloading context
     868       *
     869       * @param string $whereclause
     870       * @param array $params
     871       * @return array array of stdClass objects
     872       */
     873      protected static function get_records($whereclause, $params) {
     874          global $DB;
     875          // Retrieve from DB only the fields that need to be stored in cache.
     876          $fields = array_keys(array_filter(self::$coursecatfields));
     877          $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
     878          $sql = "SELECT cc.". join(',cc.', $fields). ", $ctxselect
     879                  FROM {course_categories} cc
     880                  JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat
     881                  WHERE ". $whereclause." ORDER BY cc.sortorder";
     882          return $DB->get_records_sql($sql,
     883                  array('contextcoursecat' => CONTEXT_COURSECAT) + $params);
     884      }
     885  
     886      /**
     887       * Resets course contact caches when role assignments were changed
     888       *
     889       * @param int $roleid role id that was given or taken away
     890       * @param context $context context where role assignment has been changed
     891       */
     892      public static function role_assignment_changed($roleid, $context) {
     893          global $CFG, $DB;
     894  
     895          if ($context->contextlevel > CONTEXT_COURSE) {
     896              // No changes to course contacts if role was assigned on the module/block level.
     897              return;
     898          }
     899  
     900          // Trigger a purge for all caches listening for changes to category enrolment.
     901          cache_helper::purge_by_event('changesincategoryenrolment');
     902  
     903          if (!$CFG->coursecontact || !in_array($roleid, explode(',', $CFG->coursecontact))) {
     904              // The role is not one of course contact roles.
     905              return;
     906          }
     907  
     908          // Remove from cache course contacts of all affected courses.
     909          $cache = cache::make('core', 'coursecontacts');
     910          if ($context->contextlevel == CONTEXT_COURSE) {
     911              $cache->delete($context->instanceid);
     912          } else if ($context->contextlevel == CONTEXT_SYSTEM) {
     913              $cache->purge();
     914          } else {
     915              $sql = "SELECT ctx.instanceid
     916                      FROM {context} ctx
     917                      WHERE ctx.path LIKE ? AND ctx.contextlevel = ?";
     918              $params = array($context->path . '/%', CONTEXT_COURSE);
     919              if ($courses = $DB->get_fieldset_sql($sql, $params)) {
     920                  $cache->delete_many($courses);
     921              }
     922          }
     923      }
     924  
     925      /**
     926       * Executed when user enrolment was changed to check if course
     927       * contacts cache needs to be cleared
     928       *
     929       * @param int $courseid course id
     930       * @param int $userid user id
     931       * @param int $status new enrolment status (0 - active, 1 - suspended)
     932       * @param int $timestart new enrolment time start
     933       * @param int $timeend new enrolment time end
     934       */
     935      public static function user_enrolment_changed($courseid, $userid,
     936              $status, $timestart = null, $timeend = null) {
     937          $cache = cache::make('core', 'coursecontacts');
     938          $contacts = $cache->get($courseid);
     939          if ($contacts === false) {
     940              // The contacts for the affected course were not cached anyway.
     941              return;
     942          }
     943          $enrolmentactive = ($status == 0) &&
     944                  (!$timestart || $timestart < time()) &&
     945                  (!$timeend || $timeend > time());
     946          if (!$enrolmentactive) {
     947              $isincontacts = false;
     948              foreach ($contacts as $contact) {
     949                  if ($contact->id == $userid) {
     950                      $isincontacts = true;
     951                  }
     952              }
     953              if (!$isincontacts) {
     954                  // Changed user's enrolment does not exist or is not active,
     955                  // and he is not in cached course contacts, no changes to be made.
     956                  return;
     957              }
     958          }
     959          // Either enrolment of manager was deleted/suspended
     960          // or user enrolment was added or activated.
     961          // In order to see if the course contacts for this course need
     962          // changing we would need to make additional queries, they will
     963          // slow down bulk enrolment changes. It is better just to remove
     964          // course contacts cache for this course.
     965          $cache->delete($courseid);
     966      }
     967  
     968      /**
     969       * Given list of DB records from table course populates each record with list of users with course contact roles
     970       *
     971       * This function fills the courses with raw information as {@link get_role_users()} would do.
     972       * See also {@link core_course_list_element::get_course_contacts()} for more readable return
     973       *
     974       * $courses[$i]->managers = array(
     975       *   $roleassignmentid => $roleuser,
     976       *   ...
     977       * );
     978       *
     979       * where $roleuser is an stdClass with the following properties:
     980       *
     981       * $roleuser->raid - role assignment id
     982       * $roleuser->id - user id
     983       * $roleuser->username
     984       * $roleuser->firstname
     985       * $roleuser->lastname
     986       * $roleuser->rolecoursealias
     987       * $roleuser->rolename
     988       * $roleuser->sortorder - role sortorder
     989       * $roleuser->roleid
     990       * $roleuser->roleshortname
     991       *
     992       * @todo MDL-38596 minimize number of queries to preload contacts for the list of courses
     993       *
     994       * @param array $courses
     995       */
     996      public static function preload_course_contacts(&$courses) {
     997          global $CFG, $DB;
     998          if (empty($courses) || empty($CFG->coursecontact)) {
     999              return;
    1000          }
    1001          $managerroles = explode(',', $CFG->coursecontact);
    1002          $cache = cache::make('core', 'coursecontacts');
    1003          $cacheddata = $cache->get_many(array_keys($courses));
    1004          $courseids = array();
    1005          foreach (array_keys($courses) as $id) {
    1006              if ($cacheddata[$id] !== false) {
    1007                  $courses[$id]->managers = $cacheddata[$id];
    1008              } else {
    1009                  $courseids[] = $id;
    1010              }
    1011          }
    1012  
    1013          // Array $courseids now stores list of ids of courses for which we still need to retrieve contacts.
    1014          if (empty($courseids)) {
    1015              return;
    1016          }
    1017  
    1018          // First build the array of all context ids of the courses and their categories.
    1019          $allcontexts = array();
    1020          foreach ($courseids as $id) {
    1021              $context = context_course::instance($id);
    1022              $courses[$id]->managers = array();
    1023              foreach (preg_split('|/|', $context->path, 0, PREG_SPLIT_NO_EMPTY) as $ctxid) {
    1024                  if (!isset($allcontexts[$ctxid])) {
    1025                      $allcontexts[$ctxid] = array();
    1026                  }
    1027                  $allcontexts[$ctxid][] = $id;
    1028              }
    1029          }
    1030  
    1031          // Fetch list of all users with course contact roles in any of the courses contexts or parent contexts.
    1032          list($sql1, $params1) = $DB->get_in_or_equal(array_keys($allcontexts), SQL_PARAMS_NAMED, 'ctxid');
    1033          list($sql2, $params2) = $DB->get_in_or_equal($managerroles, SQL_PARAMS_NAMED, 'rid');
    1034          list($sort, $sortparams) = users_order_by_sql('u');
    1035          $notdeleted = array('notdeleted' => 0);
    1036          $userfieldsapi = \core_user\fields::for_name();
    1037          $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
    1038          $sql = "SELECT ra.contextid, ra.id AS raid,
    1039                         r.id AS roleid, r.name AS rolename, r.shortname AS roleshortname,
    1040                         rn.name AS rolecoursealias, u.id, u.username, $allnames
    1041                    FROM {role_assignments} ra
    1042                    JOIN {user} u ON ra.userid = u.id
    1043                    JOIN {role} r ON ra.roleid = r.id
    1044               LEFT JOIN {role_names} rn ON (rn.contextid = ra.contextid AND rn.roleid = r.id)
    1045                  WHERE  ra.contextid ". $sql1." AND ra.roleid ". $sql2." AND u.deleted = :notdeleted
    1046               ORDER BY r.sortorder, $sort";
    1047          $rs = $DB->get_recordset_sql($sql, $params1 + $params2 + $notdeleted + $sortparams);
    1048          $checkenrolments = array();
    1049          foreach ($rs as $ra) {
    1050              foreach ($allcontexts[$ra->contextid] as $id) {
    1051                  $courses[$id]->managers[$ra->raid] = $ra;
    1052                  if (!isset($checkenrolments[$id])) {
    1053                      $checkenrolments[$id] = array();
    1054                  }
    1055                  $checkenrolments[$id][] = $ra->id;
    1056              }
    1057          }
    1058          $rs->close();
    1059  
    1060          // Remove from course contacts users who are not enrolled in the course.
    1061          $enrolleduserids = self::ensure_users_enrolled($checkenrolments);
    1062          foreach ($checkenrolments as $id => $userids) {
    1063              if (empty($enrolleduserids[$id])) {
    1064                  $courses[$id]->managers = array();
    1065              } else if ($notenrolled = array_diff($userids, $enrolleduserids[$id])) {
    1066                  foreach ($courses[$id]->managers as $raid => $ra) {
    1067                      if (in_array($ra->id, $notenrolled)) {
    1068                          unset($courses[$id]->managers[$raid]);
    1069                      }
    1070                  }
    1071              }
    1072          }
    1073  
    1074          // Set the cache.
    1075          $values = array();
    1076          foreach ($courseids as $id) {
    1077              $values[$id] = $courses[$id]->managers;
    1078          }
    1079          $cache->set_many($values);
    1080      }
    1081  
    1082      /**
    1083       * Preloads the custom fields values in bulk
    1084       *
    1085       * @param array $records
    1086       */
    1087      public static function preload_custom_fields(array &$records) {
    1088          $customfields = \core_course\customfield\course_handler::create()->get_instances_data(array_keys($records));
    1089          foreach ($customfields as $courseid => $data) {
    1090              $records[$courseid]->customfields = $data;
    1091          }
    1092      }
    1093  
    1094      /**
    1095       * Verify user enrollments for multiple course-user combinations
    1096       *
    1097       * @param array $courseusers array where keys are course ids and values are array
    1098       *     of users in this course whose enrolment we wish to verify
    1099       * @return array same structure as input array but values list only users from input
    1100       *     who are enrolled in the course
    1101       */
    1102      protected static function ensure_users_enrolled($courseusers) {
    1103          global $DB;
    1104          // If the input array is too big, split it into chunks.
    1105          $maxcoursesinquery = 20;
    1106          if (count($courseusers) > $maxcoursesinquery) {
    1107              $rv = array();
    1108              for ($offset = 0; $offset < count($courseusers); $offset += $maxcoursesinquery) {
    1109                  $chunk = array_slice($courseusers, $offset, $maxcoursesinquery, true);
    1110                  $rv = $rv + self::ensure_users_enrolled($chunk);
    1111              }
    1112              return $rv;
    1113          }
    1114  
    1115          // Create a query verifying valid user enrolments for the number of courses.
    1116          $sql = "SELECT DISTINCT e.courseid, ue.userid
    1117            FROM {user_enrolments} ue
    1118            JOIN {enrol} e ON e.id = ue.enrolid
    1119            WHERE ue.status = :active
    1120              AND e.status = :enabled
    1121              AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
    1122          $now = round(time(), -2); // Rounding helps caching in DB.
    1123          $params = array('enabled' => ENROL_INSTANCE_ENABLED,
    1124              'active' => ENROL_USER_ACTIVE,
    1125              'now1' => $now, 'now2' => $now);
    1126          $cnt = 0;
    1127          $subsqls = array();
    1128          $enrolled = array();
    1129          foreach ($courseusers as $id => $userids) {
    1130              $enrolled[$id] = array();
    1131              if (count($userids)) {
    1132                  list($sql2, $params2) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid'.$cnt.'_');
    1133                  $subsqls[] = "(e.courseid = :courseid$cnt AND ue.userid ".$sql2.")";
    1134                  $params = $params + array('courseid'.$cnt => $id) + $params2;
    1135                  $cnt++;
    1136              }
    1137          }
    1138          if (count($subsqls)) {
    1139              $sql .= "AND (". join(' OR ', $subsqls).")";
    1140              $rs = $DB->get_recordset_sql($sql, $params);
    1141              foreach ($rs as $record) {
    1142                  $enrolled[$record->courseid][] = $record->userid;
    1143              }
    1144              $rs->close();
    1145          }
    1146          return $enrolled;
    1147      }
    1148  
    1149      /**
    1150       * Retrieves number of records from course table
    1151       *
    1152       * Not all fields are retrieved. Records are ready for preloading context
    1153       *
    1154       * @param string $whereclause
    1155       * @param array $params
    1156       * @param array $options may indicate that summary needs to be retrieved
    1157       * @param bool $checkvisibility if true, capability 'moodle/course:viewhiddencourses' will be checked
    1158       *     on not visible courses and 'moodle/category:viewcourselist' on all courses
    1159       * @return array array of stdClass objects
    1160       */
    1161      protected static function get_course_records($whereclause, $params, $options, $checkvisibility = false) {
    1162          global $DB;
    1163          $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
    1164          $fields = array('c.id', 'c.category', 'c.sortorder',
    1165                          'c.shortname', 'c.fullname', 'c.idnumber',
    1166                          'c.startdate', 'c.enddate', 'c.visible', 'c.cacherev');
    1167          if (!empty($options['summary'])) {
    1168              $fields[] = 'c.summary';
    1169              $fields[] = 'c.summaryformat';
    1170          } else {
    1171              $fields[] = $DB->sql_substr('c.summary', 1, 1). ' as hassummary';
    1172          }
    1173          $sql = "SELECT ". join(',', $fields). ", $ctxselect
    1174                  FROM {course} c
    1175                  JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse
    1176                  WHERE ". $whereclause." ORDER BY c.sortorder";
    1177          $list = $DB->get_records_sql($sql,
    1178                  array('contextcourse' => CONTEXT_COURSE) + $params);
    1179  
    1180          if ($checkvisibility) {
    1181              $mycourses = enrol_get_my_courses();
    1182              // Loop through all records and make sure we only return the courses accessible by user.
    1183              foreach ($list as $course) {
    1184                  if (isset($list[$course->id]->hassummary)) {
    1185                      $list[$course->id]->hassummary = strlen($list[$course->id]->hassummary) > 0;
    1186                  }
    1187                  context_helper::preload_from_record($course);
    1188                  $context = context_course::instance($course->id);
    1189                  // Check that course is accessible by user.
    1190                  if (!array_key_exists($course->id, $mycourses) && !self::can_view_course_info($course)) {
    1191                      unset($list[$course->id]);
    1192                  }
    1193              }
    1194          }
    1195  
    1196          return $list;
    1197      }
    1198  
    1199      /**
    1200       * Returns array of ids of children categories that current user can not see
    1201       *
    1202       * This data is cached in user session cache
    1203       *
    1204       * @return array
    1205       */
    1206      protected function get_not_visible_children_ids() {
    1207          global $DB;
    1208          $coursecatcache = cache::make('core', 'coursecat');
    1209          if (($invisibleids = $coursecatcache->get('ic'. $this->id)) === false) {
    1210              // We never checked visible children before.
    1211              $hidden = self::get_tree($this->id.'i');
    1212              $catids = self::get_tree($this->id);
    1213              $invisibleids = array();
    1214              if ($catids) {
    1215                  // Preload categories contexts.
    1216                  list($sql, $params) = $DB->get_in_or_equal($catids, SQL_PARAMS_NAMED, 'id');
    1217                  $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
    1218                  $contexts = $DB->get_records_sql("SELECT $ctxselect FROM {context} ctx
    1219                      WHERE ctx.contextlevel = :contextcoursecat AND ctx.instanceid ".$sql,
    1220                          array('contextcoursecat' => CONTEXT_COURSECAT) + $params);
    1221                  foreach ($contexts as $record) {
    1222                      context_helper::preload_from_record($record);
    1223                  }
    1224                  // Check access for each category.
    1225                  foreach ($catids as $id) {
    1226                      $cat = (object)['id' => $id, 'visible' => in_array($id, $hidden) ? 0 : 1];
    1227                      if (!self::can_view_category($cat)) {
    1228                          $invisibleids[] = $id;
    1229                      }
    1230                  }
    1231              }
    1232              $coursecatcache->set('ic'. $this->id, $invisibleids);
    1233          }
    1234          return $invisibleids;
    1235      }
    1236  
    1237      /**
    1238       * Sorts list of records by several fields
    1239       *
    1240       * @param array $records array of stdClass objects
    1241       * @param array $sortfields assoc array where key is the field to sort and value is 1 for asc or -1 for desc
    1242       * @return int
    1243       */
    1244      protected static function sort_records(&$records, $sortfields) {
    1245          if (empty($records)) {
    1246              return;
    1247          }
    1248          // If sorting by course display name, calculate it (it may be fullname or shortname+fullname).
    1249          if (array_key_exists('displayname', $sortfields)) {
    1250              foreach ($records as $key => $record) {
    1251                  if (!isset($record->displayname)) {
    1252                      $records[$key]->displayname = get_course_display_name_for_list($record);
    1253                  }
    1254              }
    1255          }
    1256          // Sorting by one field - use core_collator.
    1257          if (count($sortfields) == 1) {
    1258              $property = key($sortfields);
    1259              if (in_array($property, array('sortorder', 'id', 'visible', 'parent', 'depth'))) {
    1260                  $sortflag = core_collator::SORT_NUMERIC;
    1261              } else if (in_array($property, array('idnumber', 'displayname', 'name', 'shortname', 'fullname'))) {
    1262                  $sortflag = core_collator::SORT_STRING;
    1263              } else {
    1264                  $sortflag = core_collator::SORT_REGULAR;
    1265              }
    1266              core_collator::asort_objects_by_property($records, $property, $sortflag);
    1267              if ($sortfields[$property] < 0) {
    1268                  $records = array_reverse($records, true);
    1269              }
    1270              return;
    1271          }
    1272  
    1273          // Sort by multiple fields - use custom sorting.
    1274          uasort($records, function($a, $b) use ($sortfields) {
    1275              foreach ($sortfields as $field => $mult) {
    1276                  // Nulls first.
    1277                  if (is_null($a->$field) && !is_null($b->$field)) {
    1278                      return -$mult;
    1279                  }
    1280                  if (is_null($b->$field) && !is_null($a->$field)) {
    1281                      return $mult;
    1282                  }
    1283  
    1284                  if (is_string($a->$field) || is_string($b->$field)) {
    1285                      // String fields.
    1286                      if ($cmp = strcoll($a->$field, $b->$field)) {
    1287                          return $mult * $cmp;
    1288                      }
    1289                  } else {
    1290                      // Int fields.
    1291                      if ($a->$field > $b->$field) {
    1292                          return $mult;
    1293                      }
    1294                      if ($a->$field < $b->$field) {
    1295                          return -$mult;
    1296                      }
    1297                  }
    1298              }
    1299              return 0;
    1300          });
    1301      }
    1302  
    1303      /**
    1304       * Returns array of children categories visible to the current user
    1305       *
    1306       * @param array $options options for retrieving children
    1307       *    - sort - list of fields to sort. Example
    1308       *             array('idnumber' => 1, 'name' => 1, 'id' => -1)
    1309       *             will sort by idnumber asc, name asc and id desc.
    1310       *             Default: array('sortorder' => 1)
    1311       *             Only cached fields may be used for sorting!
    1312       *    - offset
    1313       *    - limit - maximum number of children to return, 0 or null for no limit
    1314       * @return core_course_category[] Array of core_course_category objects indexed by category id
    1315       */
    1316      public function get_children($options = array()) {
    1317          global $DB;
    1318          $coursecatcache = cache::make('core', 'coursecat');
    1319  
    1320          // Get default values for options.
    1321          if (!empty($options['sort']) && is_array($options['sort'])) {
    1322              $sortfields = $options['sort'];
    1323          } else {
    1324              $sortfields = array('sortorder' => 1);
    1325          }
    1326          $limit = null;
    1327          if (!empty($options['limit']) && (int)$options['limit']) {
    1328              $limit = (int)$options['limit'];
    1329          }
    1330          $offset = 0;
    1331          if (!empty($options['offset']) && (int)$options['offset']) {
    1332              $offset = (int)$options['offset'];
    1333          }
    1334  
    1335          // First retrieve list of user-visible and sorted children ids from cache.
    1336          $sortedids = $coursecatcache->get('c'. $this->id. ':'.  serialize($sortfields));
    1337          if ($sortedids === false) {
    1338              $sortfieldskeys = array_keys($sortfields);
    1339              if ($sortfieldskeys[0] === 'sortorder') {
    1340                  // No DB requests required to build the list of ids sorted by sortorder.
    1341                  // We can easily ignore other sort fields because sortorder is always different.
    1342                  $sortedids = self::get_tree($this->id);
    1343                  if ($sortedids && ($invisibleids = $this->get_not_visible_children_ids())) {
    1344                      $sortedids = array_diff($sortedids, $invisibleids);
    1345                      if ($sortfields['sortorder'] == -1) {
    1346                          $sortedids = array_reverse($sortedids, true);
    1347                      }
    1348                  }
    1349              } else {
    1350                  // We need to retrieve and sort all children. Good thing that it is done only on first request.
    1351                  if ($invisibleids = $this->get_not_visible_children_ids()) {
    1352                      list($sql, $params) = $DB->get_in_or_equal($invisibleids, SQL_PARAMS_NAMED, 'id', false);
    1353                      $records = self::get_records('cc.parent = :parent AND cc.id '. $sql,
    1354                              array('parent' => $this->id) + $params);
    1355                  } else {
    1356                      $records = self::get_records('cc.parent = :parent', array('parent' => $this->id));
    1357                  }
    1358                  self::sort_records($records, $sortfields);
    1359                  $sortedids = array_keys($records);
    1360              }
    1361              $coursecatcache->set('c'. $this->id. ':'.serialize($sortfields), $sortedids);
    1362          }
    1363  
    1364          if (empty($sortedids)) {
    1365              return array();
    1366          }
    1367  
    1368          // Now retrieive and return categories.
    1369          if ($offset || $limit) {
    1370              $sortedids = array_slice($sortedids, $offset, $limit);
    1371          }
    1372          if (isset($records)) {
    1373              // Easy, we have already retrieved records.
    1374              if ($offset || $limit) {
    1375                  $records = array_slice($records, $offset, $limit, true);
    1376              }
    1377          } else {
    1378              list($sql, $params) = $DB->get_in_or_equal($sortedids, SQL_PARAMS_NAMED, 'id');
    1379              $records = self::get_records('cc.id '. $sql, array('parent' => $this->id) + $params);
    1380          }
    1381  
    1382          $rv = array();
    1383          foreach ($sortedids as $id) {
    1384              if (isset($records[$id])) {
    1385                  $rv[$id] = new self($records[$id]);
    1386              }
    1387          }
    1388          return $rv;
    1389      }
    1390  
    1391      /**
    1392       * Returns an array of ids of categories that are (direct and indirect) children
    1393       * of this category.
    1394       *
    1395       * @return int[]
    1396       */
    1397      public function get_all_children_ids() {
    1398          $children = [];
    1399          $walk = [$this->id];
    1400          while (count($walk) > 0) {
    1401              $catid = array_pop($walk);
    1402              $directchildren = self::get_tree($catid);
    1403              if (count($directchildren) > 0) {
    1404                  $walk = array_merge($walk, $directchildren);
    1405                  $children = array_merge($children, $directchildren);
    1406              }
    1407          }
    1408  
    1409          return $children;
    1410      }
    1411  
    1412      /**
    1413       * Returns true if the user has the manage capability on any category.
    1414       *
    1415       * This method uses the coursecat cache and an entry `has_manage_capability` to speed up
    1416       * calls to this method.
    1417       *
    1418       * @return bool
    1419       */
    1420      public static function has_manage_capability_on_any() {
    1421          return self::has_capability_on_any('moodle/category:manage');
    1422      }
    1423  
    1424      /**
    1425       * Checks if the user has at least one of the given capabilities on any category.
    1426       *
    1427       * @param array|string $capabilities One or more capabilities to check. Check made is an OR.
    1428       * @return bool
    1429       */
    1430      public static function has_capability_on_any($capabilities) {
    1431          global $DB;
    1432          if (!isloggedin() || isguestuser()) {
    1433              return false;
    1434          }
    1435  
    1436          if (!is_array($capabilities)) {
    1437              $capabilities = array($capabilities);
    1438          }
    1439          $keys = array();
    1440          foreach ($capabilities as $capability) {
    1441              $keys[$capability] = sha1($capability);
    1442          }
    1443  
    1444          /** @var cache_session $cache */
    1445          $cache = cache::make('core', 'coursecat');
    1446          $hascapability = $cache->get_many($keys);
    1447          $needtoload = false;
    1448          foreach ($hascapability as $capability) {
    1449              if ($capability === '1') {
    1450                  return true;
    1451              } else if ($capability === false) {
    1452                  $needtoload = true;
    1453              }
    1454          }
    1455          if ($needtoload === false) {
    1456              // All capabilities were retrieved and the user didn't have any.
    1457              return false;
    1458          }
    1459  
    1460          $haskey = null;
    1461          $fields = context_helper::get_preload_record_columns_sql('ctx');
    1462          $sql = "SELECT ctx.instanceid AS categoryid, $fields
    1463                        FROM {context} ctx
    1464                       WHERE contextlevel = :contextlevel
    1465                    ORDER BY depth ASC";
    1466          $params = array('contextlevel' => CONTEXT_COURSECAT);
    1467          $recordset = $DB->get_recordset_sql($sql, $params);
    1468          foreach ($recordset as $context) {
    1469              context_helper::preload_from_record($context);
    1470              $context = context_coursecat::instance($context->categoryid);
    1471              foreach ($capabilities as $capability) {
    1472                  if (has_capability($capability, $context)) {
    1473                      $haskey = $capability;
    1474                      break 2;
    1475                  }
    1476              }
    1477          }
    1478          $recordset->close();
    1479          if ($haskey === null) {
    1480              $data = array();
    1481              foreach ($keys as $key) {
    1482                  $data[$key] = '0';
    1483              }
    1484              $cache->set_many($data);
    1485              return false;
    1486          } else {
    1487              $cache->set($haskey, '1');
    1488              return true;
    1489          }
    1490      }
    1491  
    1492      /**
    1493       * Returns true if the user can resort any category.
    1494       * @return bool
    1495       */
    1496      public static function can_resort_any() {
    1497          return self::has_manage_capability_on_any();
    1498      }
    1499  
    1500      /**
    1501       * Returns true if the user can change the parent of any category.
    1502       * @return bool
    1503       */
    1504      public static function can_change_parent_any() {
    1505          return self::has_manage_capability_on_any();
    1506      }
    1507  
    1508      /**
    1509       * Returns number of subcategories visible to the current user
    1510       *
    1511       * @return int
    1512       */
    1513      public function get_children_count() {
    1514          $sortedids = self::get_tree($this->id);
    1515          $invisibleids = $this->get_not_visible_children_ids();
    1516          return count($sortedids) - count($invisibleids);
    1517      }
    1518  
    1519      /**
    1520       * Returns true if the category has ANY children, including those not visible to the user
    1521       *
    1522       * @return boolean
    1523       */
    1524      public function has_children() {
    1525          $allchildren = self::get_tree($this->id);
    1526          return !empty($allchildren);
    1527      }
    1528  
    1529      /**
    1530       * Returns true if the category has courses in it (count does not include courses
    1531       * in child categories)
    1532       *
    1533       * @return bool
    1534       */
    1535      public function has_courses() {
    1536          global $DB;
    1537          return $DB->record_exists_sql("select 1 from {course} where category = ?",
    1538                  array($this->id));
    1539      }
    1540  
    1541      /**
    1542       * Get the link used to view this course category.
    1543       *
    1544       * @return  \moodle_url
    1545       */
    1546      public function get_view_link() {
    1547          return new \moodle_url('/course/index.php', [
    1548              'categoryid' => $this->id,
    1549          ]);
    1550      }
    1551  
    1552      /**
    1553       * Searches courses
    1554       *
    1555       * List of found course ids is cached for 10 minutes. Cache may be purged prior
    1556       * to this when somebody edits courses or categories, however it is very
    1557       * difficult to keep track of all possible changes that may affect list of courses.
    1558       *
    1559       * @param array $search contains search criterias, such as:
    1560       *     - search - search string
    1561       *     - blocklist - id of block (if we are searching for courses containing specific block0
    1562       *     - modulelist - name of module (if we are searching for courses containing specific module
    1563       *     - tagid - id of tag
    1564       *     - onlywithcompletion - set to true if we only need courses with completion enabled
    1565       * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
    1566       *                       search is always category-independent
    1567       * @param array $requiredcapabilities List of capabilities required to see return course.
    1568       * @return core_course_list_element[]
    1569       */
    1570      public static function search_courses($search, $options = array(), $requiredcapabilities = array()) {
    1571          global $DB;
    1572          $offset = !empty($options['offset']) ? $options['offset'] : 0;
    1573          $limit = !empty($options['limit']) ? $options['limit'] : null;
    1574          $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1);
    1575  
    1576          $coursecatcache = cache::make('core', 'coursecat');
    1577          $cachekey = 's-'. serialize(
    1578              $search + array('sort' => $sortfields) + array('requiredcapabilities' => $requiredcapabilities)
    1579          );
    1580          $cntcachekey = 'scnt-'. serialize($search);
    1581  
    1582          $ids = $coursecatcache->get($cachekey);
    1583          if ($ids !== false) {
    1584              // We already cached last search result.
    1585              $ids = array_slice($ids, $offset, $limit);
    1586              $courses = array();
    1587              if (!empty($ids)) {
    1588                  list($sql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'id');
    1589                  $records = self::get_course_records("c.id ". $sql, $params, $options);
    1590                  // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
    1591                  if (!empty($options['coursecontacts'])) {
    1592                      self::preload_course_contacts($records);
    1593                  }
    1594                  // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
    1595                  if (!empty($options['customfields'])) {
    1596                      self::preload_custom_fields($records);
    1597                  }
    1598                  // If option 'idonly' is specified no further action is needed, just return list of ids.
    1599                  if (!empty($options['idonly'])) {
    1600                      return array_keys($records);
    1601                  }
    1602                  // Prepare the list of core_course_list_element objects.
    1603                  foreach ($ids as $id) {
    1604                      // If a course is deleted after we got the cache entry it may not exist in the database anymore.
    1605                      if (!empty($records[$id])) {
    1606                          $courses[$id] = new core_course_list_element($records[$id]);
    1607                      }
    1608                  }
    1609              }
    1610              return $courses;
    1611          }
    1612  
    1613          $preloadcoursecontacts = !empty($options['coursecontacts']);
    1614          unset($options['coursecontacts']);
    1615  
    1616          // Empty search string will return all results.
    1617          if (!isset($search['search'])) {
    1618              $search['search'] = '';
    1619          }
    1620  
    1621          if (empty($search['blocklist']) && empty($search['modulelist']) && empty($search['tagid'])) {
    1622              // Search courses that have specified words in their names/summaries.
    1623              $searchterms = preg_split('|\s+|', trim($search['search']), 0, PREG_SPLIT_NO_EMPTY);
    1624              $searchcond = $searchcondparams = [];
    1625              if (!empty($search['onlywithcompletion'])) {
    1626                  $searchcond = ['c.enablecompletion = :p1'];
    1627                  $searchcondparams = ['p1' => 1];
    1628              }
    1629              $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount,
    1630                  $requiredcapabilities, $searchcond, $searchcondparams);
    1631              self::sort_records($courselist, $sortfields);
    1632              $coursecatcache->set($cachekey, array_keys($courselist));
    1633              $coursecatcache->set($cntcachekey, $totalcount);
    1634              $records = array_slice($courselist, $offset, $limit, true);
    1635          } else {
    1636              if (!empty($search['blocklist'])) {
    1637                  // Search courses that have block with specified id.
    1638                  $blockname = $DB->get_field('block', 'name', array('id' => $search['blocklist']));
    1639                  $where = 'ctx.id in (SELECT distinct bi.parentcontextid FROM {block_instances} bi
    1640                      WHERE bi.blockname = :blockname)';
    1641                  $params = array('blockname' => $blockname);
    1642              } else if (!empty($search['modulelist'])) {
    1643                  // Search courses that have module with specified name.
    1644                  $where = "c.id IN (SELECT DISTINCT module.course ".
    1645                          "FROM {".$search['modulelist']."} module)";
    1646                  $params = array();
    1647              } else if (!empty($search['tagid'])) {
    1648                  // Search courses that are tagged with the specified tag.
    1649                  $where = "c.id IN (SELECT t.itemid ".
    1650                          "FROM {tag_instance} t WHERE t.tagid = :tagid AND t.itemtype = :itemtype AND t.component = :component)";
    1651                  $params = array('tagid' => $search['tagid'], 'itemtype' => 'course', 'component' => 'core');
    1652                  if (!empty($search['ctx'])) {
    1653                      $rec = isset($search['rec']) ? $search['rec'] : true;
    1654                      $parentcontext = context::instance_by_id($search['ctx']);
    1655                      if ($parentcontext->contextlevel == CONTEXT_SYSTEM && $rec) {
    1656                          // Parent context is system context and recursive is set to yes.
    1657                          // Nothing to filter - all courses fall into this condition.
    1658                      } else if ($rec) {
    1659                          // Filter all courses in the parent context at any level.
    1660                          $where .= ' AND ctx.path LIKE :contextpath';
    1661                          $params['contextpath'] = $parentcontext->path . '%';
    1662                      } else if ($parentcontext->contextlevel == CONTEXT_COURSECAT) {
    1663                          // All courses in the given course category.
    1664                          $where .= ' AND c.category = :category';
    1665                          $params['category'] = $parentcontext->instanceid;
    1666                      } else {
    1667                          // No courses will satisfy the context criterion, do not bother searching.
    1668                          $where = '1=0';
    1669                      }
    1670                  }
    1671              } else {
    1672                  debugging('No criteria is specified while searching courses', DEBUG_DEVELOPER);
    1673                  return array();
    1674              }
    1675              $courselist = self::get_course_records($where, $params, $options, true);
    1676              if (!empty($requiredcapabilities)) {
    1677                  foreach ($courselist as $key => $course) {
    1678                      context_helper::preload_from_record($course);
    1679                      $coursecontext = context_course::instance($course->id);
    1680                      if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
    1681                          unset($courselist[$key]);
    1682                      }
    1683                  }
    1684              }
    1685              self::sort_records($courselist, $sortfields);
    1686              $coursecatcache->set($cachekey, array_keys($courselist));
    1687              $coursecatcache->set($cntcachekey, count($courselist));
    1688              $records = array_slice($courselist, $offset, $limit, true);
    1689          }
    1690  
    1691          // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
    1692          if (!empty($preloadcoursecontacts)) {
    1693              self::preload_course_contacts($records);
    1694          }
    1695          // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
    1696          if (!empty($options['customfields'])) {
    1697              self::preload_custom_fields($records);
    1698          }
    1699          // If option 'idonly' is specified no further action is needed, just return list of ids.
    1700          if (!empty($options['idonly'])) {
    1701              return array_keys($records);
    1702          }
    1703          // Prepare the list of core_course_list_element objects.
    1704          $courses = array();
    1705          foreach ($records as $record) {
    1706              $courses[$record->id] = new core_course_list_element($record);
    1707          }
    1708          return $courses;
    1709      }
    1710  
    1711      /**
    1712       * Returns number of courses in the search results
    1713       *
    1714       * It is recommended to call this function after {@link core_course_category::search_courses()}
    1715       * and not before because only course ids are cached. Otherwise search_courses() may
    1716       * perform extra DB queries.
    1717       *
    1718       * @param array $search search criteria, see method search_courses() for more details
    1719       * @param array $options display options. They do not affect the result but
    1720       *     the 'sort' property is used in cache key for storing list of course ids
    1721       * @param array $requiredcapabilities List of capabilities required to see return course.
    1722       * @return int
    1723       */
    1724      public static function search_courses_count($search, $options = array(), $requiredcapabilities = array()) {
    1725          $coursecatcache = cache::make('core', 'coursecat');
    1726          $cntcachekey = 'scnt-'. serialize($search) . serialize($requiredcapabilities);
    1727          if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
    1728              // Cached value not found. Retrieve ALL courses and return their count.
    1729              unset($options['offset']);
    1730              unset($options['limit']);
    1731              unset($options['summary']);
    1732              unset($options['coursecontacts']);
    1733              $options['idonly'] = true;
    1734              $courses = self::search_courses($search, $options, $requiredcapabilities);
    1735              $cnt = count($courses);
    1736          }
    1737          return $cnt;
    1738      }
    1739  
    1740      /**
    1741       * Retrieves the list of courses accessible by user
    1742       *
    1743       * Not all information is cached, try to avoid calling this method
    1744       * twice in the same request.
    1745       *
    1746       * The following fields are always retrieved:
    1747       * - id, visible, fullname, shortname, idnumber, category, sortorder
    1748       *
    1749       * If you plan to use properties/methods core_course_list_element::$summary and/or
    1750       * core_course_list_element::get_course_contacts()
    1751       * you can preload this information using appropriate 'options'. Otherwise
    1752       * they will be retrieved from DB on demand and it may end with bigger DB load.
    1753       *
    1754       * Note that method core_course_list_element::has_summary() will not perform additional
    1755       * DB queries even if $options['summary'] is not specified
    1756       *
    1757       * List of found course ids is cached for 10 minutes. Cache may be purged prior
    1758       * to this when somebody edits courses or categories, however it is very
    1759       * difficult to keep track of all possible changes that may affect list of courses.
    1760       *
    1761       * @param array $options options for retrieving children
    1762       *    - recursive - return courses from subcategories as well. Use with care,
    1763       *      this may be a huge list!
    1764       *    - summary - preloads fields 'summary' and 'summaryformat'
    1765       *    - coursecontacts - preloads course contacts
    1766       *    - sort - list of fields to sort. Example
    1767       *             array('idnumber' => 1, 'shortname' => 1, 'id' => -1)
    1768       *             will sort by idnumber asc, shortname asc and id desc.
    1769       *             Default: array('sortorder' => 1)
    1770       *             Only cached fields may be used for sorting!
    1771       *    - offset
    1772       *    - limit - maximum number of children to return, 0 or null for no limit
    1773       *    - idonly - returns the array or course ids instead of array of objects
    1774       *               used only in get_courses_count()
    1775       * @return core_course_list_element[]
    1776       */
    1777      public function get_courses($options = array()) {
    1778          global $DB;
    1779          $recursive = !empty($options['recursive']);
    1780          $offset = !empty($options['offset']) ? $options['offset'] : 0;
    1781          $limit = !empty($options['limit']) ? $options['limit'] : null;
    1782          $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1);
    1783  
    1784          if (!$this->id && !$recursive) {
    1785              // There are no courses on system level unless we need recursive list.
    1786              return [];
    1787          }
    1788  
    1789          $coursecatcache = cache::make('core', 'coursecat');
    1790          $cachekey = 'l-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '').
    1791                   '-'. serialize($sortfields);
    1792          $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '');
    1793  
    1794          // Check if we have already cached results.
    1795          $ids = $coursecatcache->get($cachekey);
    1796          if ($ids !== false) {
    1797              // We already cached last search result and it did not expire yet.
    1798              $ids = array_slice($ids, $offset, $limit);
    1799              $courses = array();
    1800              if (!empty($ids)) {
    1801                  list($sql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'id');
    1802                  $records = self::get_course_records("c.id ". $sql, $params, $options);
    1803                  // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
    1804                  if (!empty($options['coursecontacts'])) {
    1805                      self::preload_course_contacts($records);
    1806                  }
    1807                  // If option 'idonly' is specified no further action is needed, just return list of ids.
    1808                  if (!empty($options['idonly'])) {
    1809                      return array_keys($records);
    1810                  }
    1811                  // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
    1812                  if (!empty($options['customfields'])) {
    1813                      self::preload_custom_fields($records);
    1814                  }
    1815                  // Prepare the list of core_course_list_element objects.
    1816                  foreach ($ids as $id) {
    1817                      // If a course is deleted after we got the cache entry it may not exist in the database anymore.
    1818                      if (!empty($records[$id])) {
    1819                          $courses[$id] = new core_course_list_element($records[$id]);
    1820                      }
    1821                  }
    1822              }
    1823              return $courses;
    1824          }
    1825  
    1826          // Retrieve list of courses in category.
    1827          $where = 'c.id <> :siteid';
    1828          $params = array('siteid' => SITEID);
    1829          if ($recursive) {
    1830              if ($this->id) {
    1831                  $context = context_coursecat::instance($this->id);
    1832                  $where .= ' AND ctx.path like :path';
    1833                  $params['path'] = $context->path. '/%';
    1834              }
    1835          } else {
    1836              $where .= ' AND c.category = :categoryid';
    1837              $params['categoryid'] = $this->id;
    1838          }
    1839          // Get list of courses without preloaded coursecontacts because we don't need them for every course.
    1840          $list = $this->get_course_records($where, $params, array_diff_key($options, array('coursecontacts' => 1)), true);
    1841  
    1842          // Sort and cache list.
    1843          self::sort_records($list, $sortfields);
    1844          $coursecatcache->set($cachekey, array_keys($list));
    1845          $coursecatcache->set($cntcachekey, count($list));
    1846  
    1847          // Apply offset/limit, convert to core_course_list_element and return.
    1848          $courses = array();
    1849          if (isset($list)) {
    1850              if ($offset || $limit) {
    1851                  $list = array_slice($list, $offset, $limit, true);
    1852              }
    1853              // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
    1854              if (!empty($options['coursecontacts'])) {
    1855                  self::preload_course_contacts($list);
    1856              }
    1857              // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
    1858              if (!empty($options['customfields'])) {
    1859                  self::preload_custom_fields($list);
    1860              }
    1861              // If option 'idonly' is specified no further action is needed, just return list of ids.
    1862              if (!empty($options['idonly'])) {
    1863                  return array_keys($list);
    1864              }
    1865              // Prepare the list of core_course_list_element objects.
    1866              foreach ($list as $record) {
    1867                  $courses[$record->id] = new core_course_list_element($record);
    1868              }
    1869          }
    1870          return $courses;
    1871      }
    1872  
    1873      /**
    1874       * Returns number of courses visible to the user
    1875       *
    1876       * @param array $options similar to get_courses() except some options do not affect
    1877       *     number of courses (i.e. sort, summary, offset, limit etc.)
    1878       * @return int
    1879       */
    1880      public function get_courses_count($options = array()) {
    1881          $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '');
    1882          $coursecatcache = cache::make('core', 'coursecat');
    1883          if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
    1884              // Cached value not found. Retrieve ALL courses and return their count.
    1885              unset($options['offset']);
    1886              unset($options['limit']);
    1887              unset($options['summary']);
    1888              unset($options['coursecontacts']);
    1889              $options['idonly'] = true;
    1890              $courses = $this->get_courses($options);
    1891              $cnt = count($courses);
    1892          }
    1893          return $cnt;
    1894      }
    1895  
    1896      /**
    1897       * Returns true if the user is able to delete this category.
    1898       *
    1899       * Note if this category contains any courses this isn't a full check, it will need to be accompanied by a call to either
    1900       * {@link core_course_category::can_delete_full()} or {@link core_course_category::can_move_content_to()}
    1901       * depending upon what the user wished to do.
    1902       *
    1903       * @return boolean
    1904       */
    1905      public function can_delete() {
    1906          if (!$this->has_manage_capability()) {
    1907              return false;
    1908          }
    1909          return $this->parent_has_manage_capability();
    1910      }
    1911  
    1912      /**
    1913       * Returns true if user can delete current category and all its contents
    1914       *
    1915       * To be able to delete course category the user must have permission
    1916       * 'moodle/category:manage' in ALL child course categories AND
    1917       * be able to delete all courses
    1918       *
    1919       * @return bool
    1920       */
    1921      public function can_delete_full() {
    1922          global $DB;
    1923          if (!$this->id) {
    1924              // Fool-proof.
    1925              return false;
    1926          }
    1927  
    1928          if (!$this->has_manage_capability()) {
    1929              return false;
    1930          }
    1931  
    1932          // Check all child categories (not only direct children).
    1933          $context = $this->get_context();
    1934          $sql = context_helper::get_preload_record_columns_sql('ctx');
    1935          $childcategories = $DB->get_records_sql('SELECT c.id, c.visible, '. $sql.
    1936              ' FROM {context} ctx '.
    1937              ' JOIN {course_categories} c ON c.id = ctx.instanceid'.
    1938              ' WHERE ctx.path like ? AND ctx.contextlevel = ?',
    1939                  array($context->path. '/%', CONTEXT_COURSECAT));
    1940          foreach ($childcategories as $childcat) {
    1941              context_helper::preload_from_record($childcat);
    1942              $childcontext = context_coursecat::instance($childcat->id);
    1943              if ((!$childcat->visible && !has_capability('moodle/category:viewhiddencategories', $childcontext)) ||
    1944                      !has_capability('moodle/category:manage', $childcontext)) {
    1945                  return false;
    1946              }
    1947          }
    1948  
    1949          // Check courses.
    1950          $sql = context_helper::get_preload_record_columns_sql('ctx');
    1951          $coursescontexts = $DB->get_records_sql('SELECT ctx.instanceid AS courseid, '.
    1952                      $sql. ' FROM {context} ctx '.
    1953                      'WHERE ctx.path like :pathmask and ctx.contextlevel = :courselevel',
    1954                  array('pathmask' => $context->path. '/%',
    1955                      'courselevel' => CONTEXT_COURSE));
    1956          foreach ($coursescontexts as $ctxrecord) {
    1957              context_helper::preload_from_record($ctxrecord);
    1958              if (!can_delete_course($ctxrecord->courseid)) {
    1959                  return false;
    1960              }
    1961          }
    1962  
    1963          // Check if plugins permit deletion of category content.
    1964          $pluginfunctions = $this->get_plugins_callback_function('can_course_category_delete');
    1965          foreach ($pluginfunctions as $pluginfunction) {
    1966              // If at least one plugin does not permit deletion, stop and return false.
    1967              if (!$pluginfunction($this)) {
    1968                  return false;
    1969              }
    1970          }
    1971  
    1972          return true;
    1973      }
    1974  
    1975      /**
    1976       * Recursively delete category including all subcategories and courses
    1977       *
    1978       * Function {@link core_course_category::can_delete_full()} MUST be called prior
    1979       * to calling this function because there is no capability check
    1980       * inside this function
    1981       *
    1982       * @param boolean $showfeedback display some notices
    1983       * @return array return deleted courses
    1984       * @throws moodle_exception
    1985       */
    1986      public function delete_full($showfeedback = true) {
    1987          global $CFG, $DB;
    1988  
    1989          require_once($CFG->libdir.'/gradelib.php');
    1990          require_once($CFG->libdir.'/questionlib.php');
    1991          require_once($CFG->dirroot.'/cohort/lib.php');
    1992  
    1993          // Make sure we won't timeout when deleting a lot of courses.
    1994          $settimeout = core_php_time_limit::raise();
    1995  
    1996          // Allow plugins to use this category before we completely delete it.
    1997          $pluginfunctions = $this->get_plugins_callback_function('pre_course_category_delete');
    1998          foreach ($pluginfunctions as $pluginfunction) {
    1999              $pluginfunction($this->get_db_record());
    2000          }
    2001  
    2002          $deletedcourses = array();
    2003  
    2004          // Get children. Note, we don't want to use cache here because it would be rebuilt too often.
    2005          $children = $DB->get_records('course_categories', array('parent' => $this->id), 'sortorder ASC');
    2006          foreach ($children as $record) {
    2007              $coursecat = new self($record);
    2008              $deletedcourses += $coursecat->delete_full($showfeedback);
    2009          }
    2010  
    2011          if ($courses = $DB->get_records('course', array('category' => $this->id), 'sortorder ASC')) {
    2012              foreach ($courses as $course) {
    2013                  if (!delete_course($course, false)) {
    2014                      throw new moodle_exception('cannotdeletecategorycourse', '', '', $course->shortname);
    2015                  }
    2016                  $deletedcourses[] = $course;
    2017              }
    2018          }
    2019  
    2020          // Move or delete cohorts in this context.
    2021          cohort_delete_category($this);
    2022  
    2023          // Now delete anything that may depend on course category context.
    2024          grade_course_category_delete($this->id, 0, $showfeedback);
    2025          $cb = new \core_contentbank\contentbank();
    2026          if (!$cb->delete_contents($this->get_context())) {
    2027              throw new moodle_exception('errordeletingcontentfromcategory', 'contentbank', '', $this->get_formatted_name());
    2028          }
    2029          if (!question_delete_course_category($this, null)) {
    2030              throw new moodle_exception('cannotdeletecategoryquestions', '', '', $this->get_formatted_name());
    2031          }
    2032  
    2033          // Delete all events in the category.
    2034          $DB->delete_records('event', array('categoryid' => $this->id));
    2035  
    2036          // Finally delete the category and it's context.
    2037          $categoryrecord = $this->get_db_record();
    2038          $DB->delete_records('course_categories', array('id' => $this->id));
    2039  
    2040          $coursecatcontext = context_coursecat::instance($this->id);
    2041          $coursecatcontext->delete();
    2042  
    2043          cache_helper::purge_by_event('changesincoursecat');
    2044  
    2045          // Trigger a course category deleted event.
    2046          /** @var \core\event\course_category_deleted $event */
    2047          $event = \core\event\course_category_deleted::create(array(
    2048              'objectid' => $this->id,
    2049              'context' => $coursecatcontext,
    2050              'other' => array('name' => $this->name)
    2051          ));
    2052          $event->add_record_snapshot($event->objecttable, $categoryrecord);
    2053          $event->set_coursecat($this);
    2054          $event->trigger();
    2055  
    2056          // If we deleted $CFG->defaultrequestcategory, make it point somewhere else.
    2057          if ($this->id == $CFG->defaultrequestcategory) {
    2058              set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0)));
    2059          }
    2060          return $deletedcourses;
    2061      }
    2062  
    2063      /**
    2064       * Checks if user can delete this category and move content (courses, subcategories and questions)
    2065       * to another category. If yes returns the array of possible target categories names
    2066       *
    2067       * If user can not manage this category or it is completely empty - empty array will be returned
    2068       *
    2069       * @return array
    2070       */
    2071      public function move_content_targets_list() {
    2072          global $CFG;
    2073          require_once($CFG->libdir . '/questionlib.php');
    2074          $context = $this->get_context();
    2075          if (!$this->is_uservisible() ||
    2076                  !has_capability('moodle/category:manage', $context)) {
    2077              // User is not able to manage current category, he is not able to delete it.
    2078              // No possible target categories.
    2079              return array();
    2080          }
    2081  
    2082          $testcaps = array();
    2083          // If this category has courses in it, user must have 'course:create' capability in target category.
    2084          if ($this->has_courses()) {
    2085              $testcaps[] = 'moodle/course:create';
    2086          }
    2087          // If this category has subcategories or questions, user must have 'category:manage' capability in target category.
    2088          if ($this->has_children() || question_context_has_any_questions($context)) {
    2089              $testcaps[] = 'moodle/category:manage';
    2090          }
    2091          if (!empty($testcaps)) {
    2092              // Return list of categories excluding this one and it's children.
    2093              return self::make_categories_list($testcaps, $this->id);
    2094          }
    2095  
    2096          // Category is completely empty, no need in target for contents.
    2097          return array();
    2098      }
    2099  
    2100      /**
    2101       * Checks if user has capability to move all category content to the new parent before
    2102       * removing this category
    2103       *
    2104       * @param int $newcatid
    2105       * @return bool
    2106       */
    2107      public function can_move_content_to($newcatid) {
    2108          global $CFG;
    2109          require_once($CFG->libdir . '/questionlib.php');
    2110  
    2111          if (!$this->has_manage_capability()) {
    2112              return false;
    2113          }
    2114  
    2115          $testcaps = array();
    2116          // If this category has courses in it, user must have 'course:create' capability in target category.
    2117          if ($this->has_courses()) {
    2118              $testcaps[] = 'moodle/course:create';
    2119          }
    2120          // If this category has subcategories or questions, user must have 'category:manage' capability in target category.
    2121          if ($this->has_children() || question_context_has_any_questions($this->get_context())) {
    2122              $testcaps[] = 'moodle/category:manage';
    2123          }
    2124          if (!empty($testcaps) && !has_all_capabilities($testcaps, context_coursecat::instance($newcatid))) {
    2125              // No sufficient capabilities to perform this task.
    2126              return false;
    2127          }
    2128  
    2129          // Check if plugins permit moving category content.
    2130          $pluginfunctions = $this->get_plugins_callback_function('can_course_category_delete_move');
    2131          $newparentcat = self::get($newcatid, MUST_EXIST, true);
    2132          foreach ($pluginfunctions as $pluginfunction) {
    2133              // If at least one plugin does not permit move on deletion, stop and return false.
    2134              if (!$pluginfunction($this, $newparentcat)) {
    2135                  return false;
    2136              }
    2137          }
    2138  
    2139          return true;
    2140      }
    2141  
    2142      /**
    2143       * Deletes a category and moves all content (children, courses and questions) to the new parent
    2144       *
    2145       * Note that this function does not check capabilities, {@link core_course_category::can_move_content_to()}
    2146       * must be called prior
    2147       *
    2148       * @param int $newparentid
    2149       * @param bool $showfeedback
    2150       * @return bool
    2151       */
    2152      public function delete_move($newparentid, $showfeedback = false) {
    2153          global $CFG, $DB, $OUTPUT;
    2154  
    2155          require_once($CFG->libdir.'/gradelib.php');
    2156          require_once($CFG->libdir.'/questionlib.php');
    2157          require_once($CFG->dirroot.'/cohort/lib.php');
    2158  
    2159          // Get all objects and lists because later the caches will be reset so.
    2160          // We don't need to make extra queries.
    2161          $newparentcat = self::get($newparentid, MUST_EXIST, true);
    2162          $catname = $this->get_formatted_name();
    2163          $children = $this->get_children();
    2164          $params = array('category' => $this->id);
    2165          $coursesids = $DB->get_fieldset_select('course', 'id', 'category = :category ORDER BY sortorder ASC', $params);
    2166          $context = $this->get_context();
    2167  
    2168          // Allow plugins to make necessary changes before we move the category content.
    2169          $pluginfunctions = $this->get_plugins_callback_function('pre_course_category_delete_move');
    2170          foreach ($pluginfunctions as $pluginfunction) {
    2171              $pluginfunction($this, $newparentcat);
    2172          }
    2173  
    2174          if ($children) {
    2175              foreach ($children as $childcat) {
    2176                  $childcat->change_parent_raw($newparentcat);
    2177                  // Log action.
    2178                  $event = \core\event\course_category_updated::create(array(
    2179                      'objectid' => $childcat->id,
    2180                      'context' => $childcat->get_context()
    2181                  ));
    2182                  $event->set_legacy_logdata(array(SITEID, 'category', 'move', 'editcategory.php?id=' . $childcat->id,
    2183                      $childcat->id));
    2184                  $event->trigger();
    2185              }
    2186              fix_course_sortorder();
    2187          }
    2188  
    2189          if ($coursesids) {
    2190              require_once($CFG->dirroot.'/course/lib.php');
    2191              if (!move_courses($coursesids, $newparentid)) {
    2192                  if ($showfeedback) {
    2193                      echo $OUTPUT->notification("Error moving courses");
    2194                  }
    2195                  return false;
    2196              }
    2197              if ($showfeedback) {
    2198                  echo $OUTPUT->notification(get_string('coursesmovedout', '', $catname), 'notifysuccess');
    2199              }
    2200          }
    2201  
    2202          // Move or delete cohorts in this context.
    2203          cohort_delete_category($this);
    2204  
    2205          // Now delete anything that may depend on course category context.
    2206          grade_course_category_delete($this->id, $newparentid, $showfeedback);
    2207          $cb = new \core_contentbank\contentbank();
    2208          $newparentcontext = context_coursecat::instance($newparentid);
    2209          $result = $cb->move_contents($context, $newparentcontext);
    2210          if ($showfeedback) {
    2211              if ($result) {
    2212                  echo $OUTPUT->notification(get_string('contentsmoved', 'contentbank', $catname), 'notifysuccess');
    2213              } else {
    2214                  echo $OUTPUT->notification(
    2215                          get_string('errordeletingcontentbankfromcategory', 'contentbank', $catname),
    2216                          'notifysuccess'
    2217                  );
    2218              }
    2219          }
    2220          if (!question_delete_course_category($this, $newparentcat)) {
    2221              if ($showfeedback) {
    2222                  echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $catname), 'notifysuccess');
    2223              }
    2224              return false;
    2225          }
    2226  
    2227          // Finally delete the category and it's context.
    2228          $categoryrecord = $this->get_db_record();
    2229          $DB->delete_records('course_categories', array('id' => $this->id));
    2230          $context->delete();
    2231  
    2232          // Trigger a course category deleted event.
    2233          /** @var \core\event\course_category_deleted $event */
    2234          $event = \core\event\course_category_deleted::create(array(
    2235              'objectid' => $this->id,
    2236              'context' => $context,
    2237              'other' => array('name' => $this->name, 'contentmovedcategoryid' => $newparentid)
    2238          ));
    2239          $event->add_record_snapshot($event->objecttable, $categoryrecord);
    2240          $event->set_coursecat($this);
    2241          $event->trigger();
    2242  
    2243          cache_helper::purge_by_event('changesincoursecat');
    2244  
    2245          if ($showfeedback) {
    2246              echo $OUTPUT->notification(get_string('coursecategorydeleted', '', $catname), 'notifysuccess');
    2247          }
    2248  
    2249          // If we deleted $CFG->defaultrequestcategory, make it point somewhere else.
    2250          if ($this->id == $CFG->defaultrequestcategory) {
    2251              set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0)));
    2252          }
    2253          return true;
    2254      }
    2255  
    2256      /**
    2257       * Checks if user can move current category to the new parent
    2258       *
    2259       * This checks if new parent category exists, user has manage cap there
    2260       * and new parent is not a child of this category
    2261       *
    2262       * @param int|stdClass|core_course_category $newparentcat
    2263       * @return bool
    2264       */
    2265      public function can_change_parent($newparentcat) {
    2266          if (!has_capability('moodle/category:manage', $this->get_context())) {
    2267              return false;
    2268          }
    2269          if (is_object($newparentcat)) {
    2270              $newparentcat = self::get($newparentcat->id, IGNORE_MISSING);
    2271          } else {
    2272              $newparentcat = self::get((int)$newparentcat, IGNORE_MISSING);
    2273          }
    2274          if (!$newparentcat) {
    2275              return false;
    2276          }
    2277          if ($newparentcat->id == $this->id || in_array($this->id, $newparentcat->get_parents())) {
    2278              // Can not move to itself or it's own child.
    2279              return false;
    2280          }
    2281          if ($newparentcat->id) {
    2282              return has_capability('moodle/category:manage', context_coursecat::instance($newparentcat->id));
    2283          } else {
    2284              return has_capability('moodle/category:manage', context_system::instance());
    2285          }
    2286      }
    2287  
    2288      /**
    2289       * Moves the category under another parent category. All associated contexts are moved as well
    2290       *
    2291       * This is protected function, use change_parent() or update() from outside of this class
    2292       *
    2293       * @see core_course_category::change_parent()
    2294       * @see core_course_category::update()
    2295       *
    2296       * @param core_course_category $newparentcat
    2297       * @throws moodle_exception
    2298       */
    2299      protected function change_parent_raw(core_course_category $newparentcat) {
    2300          global $DB;
    2301  
    2302          $context = $this->get_context();
    2303  
    2304          $hidecat = false;
    2305          if (empty($newparentcat->id)) {
    2306              $DB->set_field('course_categories', 'parent', 0, array('id' => $this->id));
    2307              $newparent = context_system::instance();
    2308          } else {
    2309              if ($newparentcat->id == $this->id || in_array($this->id, $newparentcat->get_parents())) {
    2310                  // Can not move to itself or it's own child.
    2311                  throw new moodle_exception('cannotmovecategory');
    2312              }
    2313              $DB->set_field('course_categories', 'parent', $newparentcat->id, array('id' => $this->id));
    2314              $newparent = context_coursecat::instance($newparentcat->id);
    2315  
    2316              if (!$newparentcat->visible and $this->visible) {
    2317                  // Better hide category when moving into hidden category, teachers may unhide afterwards and the hidden children
    2318                  // will be restored properly.
    2319                  $hidecat = true;
    2320              }
    2321          }
    2322          $this->parent = $newparentcat->id;
    2323  
    2324          $context->update_moved($newparent);
    2325  
    2326          // Now make it last in new category.
    2327          $DB->set_field('course_categories', 'sortorder',
    2328              get_max_courses_in_category() * MAX_COURSE_CATEGORIES, ['id' => $this->id]);
    2329  
    2330          if ($hidecat) {
    2331              fix_course_sortorder();
    2332              $this->restore();
    2333              // Hide object but store 1 in visibleold, because when parent category visibility changes this category must
    2334              // become visible again.
    2335              $this->hide_raw(1);
    2336          }
    2337      }
    2338  
    2339      /**
    2340       * Efficiently moves a category - NOTE that this can have
    2341       * a huge impact access-control-wise...
    2342       *
    2343       * Note that this function does not check capabilities.
    2344       *
    2345       * Example of usage:
    2346       * $coursecat = core_course_category::get($categoryid);
    2347       * if ($coursecat->can_change_parent($newparentcatid)) {
    2348       *     $coursecat->change_parent($newparentcatid);
    2349       * }
    2350       *
    2351       * This function does not update field course_categories.timemodified
    2352       * If you want to update timemodified, use
    2353       * $coursecat->update(array('parent' => $newparentcat));
    2354       *
    2355       * @param int|stdClass|core_course_category $newparentcat
    2356       */
    2357      public function change_parent($newparentcat) {
    2358          // Make sure parent category exists but do not check capabilities here that it is visible to current user.
    2359          if (is_object($newparentcat)) {
    2360              $newparentcat = self::get($newparentcat->id, MUST_EXIST, true);
    2361          } else {
    2362              $newparentcat = self::get((int)$newparentcat, MUST_EXIST, true);
    2363          }
    2364          if ($newparentcat->id != $this->parent) {
    2365              $this->change_parent_raw($newparentcat);
    2366              fix_course_sortorder();
    2367              cache_helper::purge_by_event('changesincoursecat');
    2368              $this->restore();
    2369  
    2370              $event = \core\event\course_category_updated::create(array(
    2371                  'objectid' => $this->id,
    2372                  'context' => $this->get_context()
    2373              ));
    2374              $event->set_legacy_logdata(array(SITEID, 'category', 'move', 'editcategory.php?id=' . $this->id, $this->id));
    2375              $event->trigger();
    2376          }
    2377      }
    2378  
    2379      /**
    2380       * Hide course category and child course and subcategories
    2381       *
    2382       * If this category has changed the parent and is moved under hidden
    2383       * category we will want to store it's current visibility state in
    2384       * the field 'visibleold'. If admin clicked 'hide' for this particular
    2385       * category, the field 'visibleold' should become 0.
    2386       *
    2387       * All subcategories and courses will have their current visibility in the field visibleold
    2388       *
    2389       * This is protected function, use hide() or update() from outside of this class
    2390       *
    2391       * @see core_course_category::hide()
    2392       * @see core_course_category::update()
    2393       *
    2394       * @param int $visibleold value to set in field $visibleold for this category
    2395       * @return bool whether changes have been made and caches need to be purged afterwards
    2396       */
    2397      protected function hide_raw($visibleold = 0) {
    2398          global $DB;
    2399          $changes = false;
    2400  
    2401          // Note that field 'visibleold' is not cached so we must retrieve it from DB if it is missing.
    2402          if ($this->id && $this->__get('visibleold') != $visibleold) {
    2403              $this->visibleold = $visibleold;
    2404              $DB->set_field('course_categories', 'visibleold', $visibleold, array('id' => $this->id));
    2405              $changes = true;
    2406          }
    2407          if (!$this->visible || !$this->id) {
    2408              // Already hidden or can not be hidden.
    2409              return $changes;
    2410          }
    2411  
    2412          $this->visible = 0;
    2413          $DB->set_field('course_categories', 'visible', 0, array('id' => $this->id));
    2414          // Store visible flag so that we can return to it if we immediately unhide.
    2415          $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($this->id));
    2416          $DB->set_field('course', 'visible', 0, array('category' => $this->id));
    2417          // Get all child categories and hide too.
    2418          if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$this->path/%"), 'id, visible')) {
    2419              foreach ($subcats as $cat) {
    2420                  $DB->set_field('course_categories', 'visibleold', $cat->visible, array('id' => $cat->id));
    2421                  $DB->set_field('course_categories', 'visible', 0, array('id' => $cat->id));
    2422                  $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($cat->id));
    2423                  $DB->set_field('course', 'visible', 0, array('category' => $cat->id));
    2424              }
    2425          }
    2426          return true;
    2427      }
    2428  
    2429      /**
    2430       * Hide course category and child course and subcategories
    2431       *
    2432       * Note that there is no capability check inside this function
    2433       *
    2434       * This function does not update field course_categories.timemodified
    2435       * If you want to update timemodified, use
    2436       * $coursecat->update(array('visible' => 0));
    2437       */
    2438      public function hide() {
    2439          if ($this->hide_raw(0)) {
    2440              cache_helper::purge_by_event('changesincoursecat');
    2441  
    2442              $event = \core\event\course_category_updated::create(array(
    2443                  'objectid' => $this->id,
    2444                  'context' => $this->get_context()
    2445              ));
    2446              $event->set_legacy_logdata(array(SITEID, 'category', 'hide', 'editcategory.php?id=' . $this->id, $this->id));
    2447              $event->trigger();
    2448          }
    2449      }
    2450  
    2451      /**
    2452       * Show course category and restores visibility for child course and subcategories
    2453       *
    2454       * Note that there is no capability check inside this function
    2455       *
    2456       * This is protected function, use show() or update() from outside of this class
    2457       *
    2458       * @see core_course_category::show()
    2459       * @see core_course_category::update()
    2460       *
    2461       * @return bool whether changes have been made and caches need to be purged afterwards
    2462       */
    2463      protected function show_raw() {
    2464          global $DB;
    2465  
    2466          if ($this->visible) {
    2467              // Already visible.
    2468              return false;
    2469          }
    2470  
    2471          $this->visible = 1;
    2472          $this->visibleold = 1;
    2473          $DB->set_field('course_categories', 'visible', 1, array('id' => $this->id));
    2474          $DB->set_field('course_categories', 'visibleold', 1, array('id' => $this->id));
    2475          $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($this->id));
    2476          // Get all child categories and unhide too.
    2477          if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$this->path/%"), 'id, visibleold')) {
    2478              foreach ($subcats as $cat) {
    2479                  if ($cat->visibleold) {
    2480                      $DB->set_field('course_categories', 'visible', 1, array('id' => $cat->id));
    2481                  }
    2482                  $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($cat->id));
    2483              }
    2484          }
    2485          return true;
    2486      }
    2487  
    2488      /**
    2489       * Show course category and restores visibility for child course and subcategories
    2490       *
    2491       * Note that there is no capability check inside this function
    2492       *
    2493       * This function does not update field course_categories.timemodified
    2494       * If you want to update timemodified, use
    2495       * $coursecat->update(array('visible' => 1));
    2496       */
    2497      public function show() {
    2498          if ($this->show_raw()) {
    2499              cache_helper::purge_by_event('changesincoursecat');
    2500  
    2501              $event = \core\event\course_category_updated::create(array(
    2502                  'objectid' => $this->id,
    2503                  'context' => $this->get_context()
    2504              ));
    2505              $event->set_legacy_logdata(array(SITEID, 'category', 'show', 'editcategory.php?id=' . $this->id, $this->id));
    2506              $event->trigger();
    2507          }
    2508      }
    2509  
    2510      /**
    2511       * Returns name of the category formatted as a string
    2512       *
    2513       * @param array $options formatting options other than context
    2514       * @return string
    2515       */
    2516      public function get_formatted_name($options = array()) {
    2517          if ($this->id) {
    2518              $context = $this->get_context();
    2519              return format_string($this->name, true, array('context' => $context) + $options);
    2520          } else {
    2521              return get_string('top');
    2522          }
    2523      }
    2524  
    2525      /**
    2526       * Get the nested name of this category, with all of it's parents.
    2527       *
    2528       * @param   bool    $includelinks Whether to wrap each name in the view link for that category.
    2529       * @param   string  $separator The string between each name.
    2530       * @param   array   $options Formatting options.
    2531       * @return  string
    2532       */
    2533      public function get_nested_name($includelinks = true, $separator = ' / ', $options = []) {
    2534          // Get the name of hierarchical name of this category.
    2535          $parents = $this->get_parents();
    2536          $categories = static::get_many($parents);
    2537          $categories[] = $this;
    2538  
    2539          $names = array_map(function($category) use ($options, $includelinks) {
    2540              if ($includelinks) {
    2541                  return html_writer::link($category->get_view_link(), $category->get_formatted_name($options));
    2542              } else {
    2543                  return $category->get_formatted_name($options);
    2544              }
    2545  
    2546          }, $categories);
    2547  
    2548          return implode($separator, $names);
    2549      }
    2550  
    2551      /**
    2552       * Returns ids of all parents of the category. Last element in the return array is the direct parent
    2553       *
    2554       * For example, if you have a tree of categories like:
    2555       *   Miscellaneous (id = 1)
    2556       *      Subcategory (id = 2)
    2557       *         Sub-subcategory (id = 4)
    2558       *   Other category (id = 3)
    2559       *
    2560       * core_course_category::get(1)->get_parents() == array()
    2561       * core_course_category::get(2)->get_parents() == array(1)
    2562       * core_course_category::get(4)->get_parents() == array(1, 2);
    2563       *
    2564       * Note that this method does not check if all parents are accessible by current user
    2565       *
    2566       * @return array of category ids
    2567       */
    2568      public function get_parents() {
    2569          $parents = preg_split('|/|', $this->path, 0, PREG_SPLIT_NO_EMPTY);
    2570          array_pop($parents);
    2571          return $parents;
    2572      }
    2573  
    2574      /**
    2575       * This function returns a nice list representing category tree
    2576       * for display or to use in a form <select> element
    2577       *
    2578       * List is cached for 10 minutes
    2579       *
    2580       * For example, if you have a tree of categories like:
    2581       *   Miscellaneous (id = 1)
    2582       *      Subcategory (id = 2)
    2583       *         Sub-subcategory (id = 4)
    2584       *   Other category (id = 3)
    2585       * Then after calling this function you will have
    2586       * array(1 => 'Miscellaneous',
    2587       *       2 => 'Miscellaneous / Subcategory',
    2588       *       4 => 'Miscellaneous / Subcategory / Sub-subcategory',
    2589       *       3 => 'Other category');
    2590       *
    2591       * If you specify $requiredcapability, then only categories where the current
    2592       * user has that capability will be added to $list.
    2593       * If you only have $requiredcapability in a child category, not the parent,
    2594       * then the child catgegory will still be included.
    2595       *
    2596       * If you specify the option $excludeid, then that category, and all its children,
    2597       * are omitted from the tree. This is useful when you are doing something like
    2598       * moving categories, where you do not want to allow people to move a category
    2599       * to be the child of itself.
    2600       *
    2601       * @param string/array $requiredcapability if given, only categories where the current
    2602       *      user has this capability will be returned. Can also be an array of capabilities,
    2603       *      in which case they are all required.
    2604       * @param integer $excludeid Exclude this category and its children from the lists built.
    2605       * @param string $separator string to use as a separator between parent and child category. Default ' / '
    2606       * @return array of strings
    2607       */
    2608      public static function make_categories_list($requiredcapability = '', $excludeid = 0, $separator = ' / ') {
    2609          global $DB;
    2610          $coursecatcache = cache::make('core', 'coursecat');
    2611  
    2612          // Check if we cached the complete list of user-accessible category names ($baselist) or list of ids
    2613          // with requried cap ($thislist).
    2614          $currentlang = current_language();
    2615          $basecachekey = $currentlang . '_catlist';
    2616          $baselist = $coursecatcache->get($basecachekey);
    2617          $thislist = false;
    2618          $thiscachekey = null;
    2619          if (!empty($requiredcapability)) {
    2620              $requiredcapability = (array)$requiredcapability;
    2621              $thiscachekey = 'catlist:'. serialize($requiredcapability);
    2622              if ($baselist !== false && ($thislist = $coursecatcache->get($thiscachekey)) !== false) {
    2623                  $thislist = preg_split('|,|', $thislist, -1, PREG_SPLIT_NO_EMPTY);
    2624              }
    2625          } else if ($baselist !== false) {
    2626              $thislist = array_keys(array_filter($baselist, function($el) {
    2627                  return $el['name'] !== false;
    2628              }));
    2629          }
    2630  
    2631          if ($baselist === false) {
    2632              // We don't have $baselist cached, retrieve it. Retrieve $thislist again in any case.
    2633              $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
    2634              $sql = "SELECT cc.id, cc.sortorder, cc.name, cc.visible, cc.parent, cc.path, $ctxselect
    2635                      FROM {course_categories} cc
    2636                      JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat
    2637                      ORDER BY cc.sortorder";
    2638              $rs = $DB->get_recordset_sql($sql, array('contextcoursecat' => CONTEXT_COURSECAT));
    2639              $baselist = array();
    2640              $thislist = array();
    2641              foreach ($rs as $record) {
    2642                  context_helper::preload_from_record($record);
    2643                  $context = context_coursecat::instance($record->id);
    2644                  $canview = self::can_view_category($record);
    2645                  $baselist[$record->id] = array(
    2646                      'name' => $canview ? format_string($record->name, true, array('context' => $context)) : false,
    2647                      'path' => $record->path
    2648                  );
    2649                  if (!$canview || (!empty($requiredcapability) && !has_all_capabilities($requiredcapability, $context))) {
    2650                      // No required capability, added to $baselist but not to $thislist.
    2651                      continue;
    2652                  }
    2653                  $thislist[] = $record->id;
    2654              }
    2655              $rs->close();
    2656              $coursecatcache->set($basecachekey, $baselist);
    2657              if (!empty($requiredcapability)) {
    2658                  $coursecatcache->set($thiscachekey, join(',', $thislist));
    2659              }
    2660          } else if ($thislist === false) {
    2661              // We have $baselist cached but not $thislist. Simplier query is used to retrieve.
    2662              $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
    2663              $sql = "SELECT ctx.instanceid AS id, $ctxselect
    2664                      FROM {context} ctx WHERE ctx.contextlevel = :contextcoursecat";
    2665              $contexts = $DB->get_records_sql($sql, array('contextcoursecat' => CONTEXT_COURSECAT));
    2666              $thislist = array();
    2667              foreach (array_keys($baselist) as $id) {
    2668                  if ($baselist[$id]['name'] !== false) {
    2669                      context_helper::preload_from_record($contexts[$id]);
    2670                      if (has_all_capabilities($requiredcapability, context_coursecat::instance($id))) {
    2671                          $thislist[] = $id;
    2672                      }
    2673                  }
    2674              }
    2675              $coursecatcache->set($thiscachekey, join(',', $thislist));
    2676          }
    2677  
    2678          // Now build the array of strings to return, mind $separator and $excludeid.
    2679          $names = array();
    2680          foreach ($thislist as $id) {
    2681              $path = preg_split('|/|', $baselist[$id]['path'], -1, PREG_SPLIT_NO_EMPTY);
    2682              if (!$excludeid || !in_array($excludeid, $path)) {
    2683                  $namechunks = array();
    2684                  foreach ($path as $parentid) {
    2685                      if (array_key_exists($parentid, $baselist) && $baselist[$parentid]['name'] !== false) {
    2686                          $namechunks[] = $baselist[$parentid]['name'];
    2687                      }
    2688                  }
    2689                  $names[$id] = join($separator, $namechunks);
    2690              }
    2691          }
    2692          return $names;
    2693      }
    2694  
    2695      /**
    2696       * Prepares the object for caching. Works like the __sleep method.
    2697       *
    2698       * implementing method from interface cacheable_object
    2699       *
    2700       * @return array ready to be cached
    2701       */
    2702      public function prepare_to_cache() {
    2703          $a = array();
    2704          foreach (self::$coursecatfields as $property => $cachedirectives) {
    2705              if ($cachedirectives !== null) {
    2706                  list($shortname, $defaultvalue) = $cachedirectives;
    2707                  if ($this->$property !== $defaultvalue) {
    2708                      $a[$shortname] = $this->$property;
    2709                  }
    2710              }
    2711          }
    2712          $context = $this->get_context();
    2713          $a['xi'] = $context->id;
    2714          $a['xp'] = $context->path;
    2715          $a['xl'] = $context->locked;
    2716          return $a;
    2717      }
    2718  
    2719      /**
    2720       * Takes the data provided by prepare_to_cache and reinitialises an instance of the associated from it.
    2721       *
    2722       * implementing method from interface cacheable_object
    2723       *
    2724       * @param array $a
    2725       * @return core_course_category
    2726       */
    2727      public static function wake_from_cache($a) {
    2728          $record = new stdClass;
    2729          foreach (self::$coursecatfields as $property => $cachedirectives) {
    2730              if ($cachedirectives !== null) {
    2731                  list($shortname, $defaultvalue) = $cachedirectives;
    2732                  if (array_key_exists($shortname, $a)) {
    2733                      $record->$property = $a[$shortname];
    2734                  } else {
    2735                      $record->$property = $defaultvalue;
    2736                  }
    2737              }
    2738          }
    2739          $record->ctxid = $a['xi'];
    2740          $record->ctxpath = $a['xp'];
    2741          $record->ctxdepth = $record->depth + 1;
    2742          $record->ctxlevel = CONTEXT_COURSECAT;
    2743          $record->ctxinstance = $record->id;
    2744          $record->ctxlocked = $a['xl'];
    2745          return new self($record, true);
    2746      }
    2747  
    2748      /**
    2749       * Returns true if the user is able to create a top level category.
    2750       * @return bool
    2751       */
    2752      public static function can_create_top_level_category() {
    2753          return self::top()->has_manage_capability();
    2754      }
    2755  
    2756      /**
    2757       * Returns the category context.
    2758       * @return context_coursecat
    2759       */
    2760      public function get_context() {
    2761          if ($this->id === 0) {
    2762              // This is the special top level category object.
    2763              return context_system::instance();
    2764          } else {
    2765              return context_coursecat::instance($this->id);
    2766          }
    2767      }
    2768  
    2769      /**
    2770       * Returns true if the user is able to manage this category.
    2771       * @return bool
    2772       */
    2773      public function has_manage_capability() {
    2774          if (!$this->is_uservisible()) {
    2775              return false;
    2776          }
    2777          return has_capability('moodle/category:manage', $this->get_context());
    2778      }
    2779  
    2780      /**
    2781       * Returns true if the user has the manage capability on the parent category.
    2782       * @return bool
    2783       */
    2784      public function parent_has_manage_capability() {
    2785          return ($parent = $this->get_parent_coursecat()) && $parent->has_manage_capability();
    2786      }
    2787  
    2788      /**
    2789       * Returns true if the current user can create subcategories of this category.
    2790       * @return bool
    2791       */
    2792      public function can_create_subcategory() {
    2793          return $this->has_manage_capability();
    2794      }
    2795  
    2796      /**
    2797       * Returns true if the user can resort this categories sub categories and courses.
    2798       * Must have manage capability and be able to see all subcategories.
    2799       * @return bool
    2800       */
    2801      public function can_resort_subcategories() {
    2802          return $this->has_manage_capability() && !$this->get_not_visible_children_ids();
    2803      }
    2804  
    2805      /**
    2806       * Returns true if the user can resort the courses within this category.
    2807       * Must have manage capability and be able to see all courses.
    2808       * @return bool
    2809       */
    2810      public function can_resort_courses() {
    2811          return $this->has_manage_capability() && $this->coursecount == $this->get_courses_count();
    2812      }
    2813  
    2814      /**
    2815       * Returns true of the user can change the sortorder of this category (resort in the parent category)
    2816       * @return bool
    2817       */
    2818      public function can_change_sortorder() {
    2819          return ($parent = $this->get_parent_coursecat()) && $parent->can_resort_subcategories();
    2820      }
    2821  
    2822      /**
    2823       * Returns true if the current user can create a course within this category.
    2824       * @return bool
    2825       */
    2826      public function can_create_course() {
    2827          return $this->is_uservisible() && has_capability('moodle/course:create', $this->get_context());
    2828      }
    2829  
    2830      /**
    2831       * Returns true if the current user can edit this categories settings.
    2832       * @return bool
    2833       */
    2834      public function can_edit() {
    2835          return $this->has_manage_capability();
    2836      }
    2837  
    2838      /**
    2839       * Returns true if the current user can review role assignments for this category.
    2840       * @return bool
    2841       */
    2842      public function can_review_roles() {
    2843          return $this->is_uservisible() && has_capability('moodle/role:assign', $this->get_context());
    2844      }
    2845  
    2846      /**
    2847       * Returns true if the current user can review permissions for this category.
    2848       * @return bool
    2849       */
    2850      public function can_review_permissions() {
    2851          return $this->is_uservisible() &&
    2852          has_any_capability(array(
    2853              'moodle/role:assign',
    2854              'moodle/role:safeoverride',
    2855              'moodle/role:override',
    2856              'moodle/role:assign'
    2857          ), $this->get_context());
    2858      }
    2859  
    2860      /**
    2861       * Returns true if the current user can review cohorts for this category.
    2862       * @return bool
    2863       */
    2864      public function can_review_cohorts() {
    2865          return $this->is_uservisible() &&
    2866              has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $this->get_context());
    2867      }
    2868  
    2869      /**
    2870       * Returns true if the current user can review filter settings for this category.
    2871       * @return bool
    2872       */
    2873      public function can_review_filters() {
    2874          return $this->is_uservisible() &&
    2875                  has_capability('moodle/filter:manage', $this->get_context()) &&
    2876                  count(filter_get_available_in_context($this->get_context())) > 0;
    2877      }
    2878  
    2879      /**
    2880       * Returns true if the current user is able to change the visbility of this category.
    2881       * @return bool
    2882       */
    2883      public function can_change_visibility() {
    2884          return $this->parent_has_manage_capability();
    2885      }
    2886  
    2887      /**
    2888       * Returns true if the user can move courses out of this category.
    2889       * @return bool
    2890       */
    2891      public function can_move_courses_out_of() {
    2892          return $this->has_manage_capability();
    2893      }
    2894  
    2895      /**
    2896       * Returns true if the user can move courses into this category.
    2897       * @return bool
    2898       */
    2899      public function can_move_courses_into() {
    2900          return $this->has_manage_capability();
    2901      }
    2902  
    2903      /**
    2904       * Returns true if the user is able to restore a course into this category as a new course.
    2905       * @return bool
    2906       */
    2907      public function can_restore_courses_into() {
    2908          return $this->is_uservisible() && has_capability('moodle/restore:restorecourse', $this->get_context());
    2909      }
    2910  
    2911      /**
    2912       * Resorts the sub categories of this category by the given field.
    2913       *
    2914       * @param string $field One of name, idnumber or descending values of each (appended desc)
    2915       * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
    2916       * @return bool True on success.
    2917       * @throws coding_exception
    2918       */
    2919      public function resort_subcategories($field, $cleanup = true) {
    2920          global $DB;
    2921          $desc = false;
    2922          if (substr($field, -4) === "desc") {
    2923              $desc = true;
    2924              $field = substr($field, 0, -4);  // Remove "desc" from field name.
    2925          }
    2926          if ($field !== 'name' && $field !== 'idnumber') {
    2927              throw new coding_exception('Invalid field requested');
    2928          }
    2929          $children = $this->get_children();
    2930          core_collator::asort_objects_by_property($children, $field, core_collator::SORT_NATURAL);
    2931          if (!empty($desc)) {
    2932              $children = array_reverse($children);
    2933          }
    2934          $i = 1;
    2935          foreach ($children as $cat) {
    2936              $i++;
    2937              $DB->set_field('course_categories', 'sortorder', $i, array('id' => $cat->id));
    2938              $i += $cat->coursecount;
    2939          }
    2940          if ($cleanup) {
    2941              self::resort_categories_cleanup();
    2942          }
    2943          return true;
    2944      }
    2945  
    2946      /**
    2947       * Cleans things up after categories have been resorted.
    2948       * @param bool $includecourses If set to true we know courses have been resorted as well.
    2949       */
    2950      public static function resort_categories_cleanup($includecourses = false) {
    2951          // This should not be needed but we do it just to be safe.
    2952          fix_course_sortorder();
    2953          cache_helper::purge_by_event('changesincoursecat');
    2954          if ($includecourses) {
    2955              cache_helper::purge_by_event('changesincourse');
    2956          }
    2957      }
    2958  
    2959      /**
    2960       * Resort the courses within this category by the given field.
    2961       *
    2962       * @param string $field One of fullname, shortname, idnumber or descending values of each (appended desc)
    2963       * @param bool $cleanup
    2964       * @return bool True for success.
    2965       * @throws coding_exception
    2966       */
    2967      public function resort_courses($field, $cleanup = true) {
    2968          global $DB;
    2969          $desc = false;
    2970          if (substr($field, -4) === "desc") {
    2971              $desc = true;
    2972              $field = substr($field, 0, -4);  // Remove "desc" from field name.
    2973          }
    2974          if ($field !== 'fullname' && $field !== 'shortname' && $field !== 'idnumber' && $field !== 'timecreated') {
    2975              // This is ultra important as we use $field in an SQL statement below this.
    2976              throw new coding_exception('Invalid field requested');
    2977          }
    2978          $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
    2979          $sql = "SELECT c.id, c.sortorder, c.{$field}, $ctxfields
    2980                    FROM {course} c
    2981               LEFT JOIN {context} ctx ON ctx.instanceid = c.id
    2982                   WHERE ctx.contextlevel = :ctxlevel AND
    2983                         c.category = :categoryid";
    2984          $params = array(
    2985              'ctxlevel' => CONTEXT_COURSE,
    2986              'categoryid' => $this->id
    2987          );
    2988          $courses = $DB->get_records_sql($sql, $params);
    2989          if (count($courses) > 0) {
    2990              foreach ($courses as $courseid => $course) {
    2991                  context_helper::preload_from_record($course);
    2992                  if ($field === 'idnumber') {
    2993                      $course->sortby = $course->idnumber;
    2994                  } else {
    2995                      // It'll require formatting.
    2996                      $options = array(
    2997                          'context' => context_course::instance($course->id)
    2998                      );
    2999                      // We format the string first so that it appears as the user would see it.
    3000                      // This ensures the sorting makes sense to them. However it won't necessarily make
    3001                      // sense to everyone if things like multilang filters are enabled.
    3002                      // We then strip any tags as we don't want things such as image tags skewing the
    3003                      // sort results.
    3004                      $course->sortby = strip_tags(format_string($course->$field, true, $options));
    3005                  }
    3006                  // We set it back here rather than using references as there is a bug with using
    3007                  // references in a foreach before passing as an arg by reference.
    3008                  $courses[$courseid] = $course;
    3009              }
    3010              // Sort the courses.
    3011              core_collator::asort_objects_by_property($courses, 'sortby', core_collator::SORT_NATURAL);
    3012              if (!empty($desc)) {
    3013                  $courses = array_reverse($courses);
    3014              }
    3015              $i = 1;
    3016              foreach ($courses as $course) {
    3017                  $DB->set_field('course', 'sortorder', $this->sortorder + $i, array('id' => $course->id));
    3018                  $i++;
    3019              }
    3020              if ($cleanup) {
    3021                  // This should not be needed but we do it just to be safe.
    3022                  fix_course_sortorder();
    3023                  cache_helper::purge_by_event('changesincourse');
    3024              }
    3025          }
    3026          return true;
    3027      }
    3028  
    3029      /**
    3030       * Changes the sort order of this categories parent shifting this category up or down one.
    3031       *
    3032       * @param bool $up If set to true the category is shifted up one spot, else its moved down.
    3033       * @return bool True on success, false otherwise.
    3034       */
    3035      public function change_sortorder_by_one($up) {
    3036          global $DB;
    3037          $params = array($this->sortorder, $this->parent);
    3038          if ($up) {
    3039              $select = 'sortorder < ? AND parent = ?';
    3040              $sort = 'sortorder DESC';
    3041          } else {
    3042              $select = 'sortorder > ? AND parent = ?';
    3043              $sort = 'sortorder ASC';
    3044          }
    3045          fix_course_sortorder();
    3046          $swapcategory = $DB->get_records_select('course_categories', $select, $params, $sort, '*', 0, 1);
    3047          $swapcategory = reset($swapcategory);
    3048          if ($swapcategory) {
    3049              $DB->set_field('course_categories', 'sortorder', $swapcategory->sortorder, array('id' => $this->id));
    3050              $DB->set_field('course_categories', 'sortorder', $this->sortorder, array('id' => $swapcategory->id));
    3051              $this->sortorder = $swapcategory->sortorder;
    3052  
    3053              $event = \core\event\course_category_updated::create(array(
    3054                  'objectid' => $this->id,
    3055                  'context' => $this->get_context()
    3056              ));
    3057              $event->set_legacy_logdata(array(SITEID, 'category', 'move', 'management.php?categoryid=' . $this->id,
    3058                  $this->id));
    3059              $event->trigger();
    3060  
    3061              // Finally reorder courses.
    3062              fix_course_sortorder();
    3063              cache_helper::purge_by_event('changesincoursecat');
    3064              return true;
    3065          }
    3066          return false;
    3067      }
    3068  
    3069      /**
    3070       * Returns the parent core_course_category object for this category.
    3071       *
    3072       * Only returns parent if it exists and is visible to the current user
    3073       *
    3074       * @return core_course_category|null
    3075       */
    3076      public function get_parent_coursecat() {
    3077          if (!$this->id) {
    3078              return null;
    3079          }
    3080          return self::get($this->parent, IGNORE_MISSING);
    3081      }
    3082  
    3083  
    3084      /**
    3085       * Returns true if the user is able to request a new course be created.
    3086       * @return bool
    3087       */
    3088      public function can_request_course() {
    3089          return course_request::can_request($this->get_context());
    3090      }
    3091  
    3092      /**
    3093       * Returns true if the user can approve course requests.
    3094       * @return bool
    3095       */
    3096      public static function can_approve_course_requests() {
    3097          global $CFG, $DB;
    3098          if (empty($CFG->enablecourserequests)) {
    3099              return false;
    3100          }
    3101          $context = context_system::instance();
    3102          if (!has_capability('moodle/site:approvecourse', $context)) {
    3103              return false;
    3104          }
    3105          if (!$DB->record_exists('course_request', array())) {
    3106              return false;
    3107          }
    3108          return true;
    3109      }
    3110  }