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