[ Index ]

PHP Cross Reference of Unnamed Project

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1