[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/ -> coursecatlib.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1