Search moodle.org's
Developer Documentation


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

Search This Site: