Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • Differences Between: [Versions 28 and 30] [Versions 28 and 31] [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35] [Versions 28 and 36] [Versions 28 and 37]

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

    Search This Site: