Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]
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 * This file contains classes used to manage the navigation structures within Moodle. 19 * 20 * @since Moodle 2.0 21 * @package core 22 * @copyright 2009 Sam Hemelryk 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 use core\moodlenet\utilities; 27 use core_contentbank\contentbank; 28 29 defined('MOODLE_INTERNAL') || die(); 30 31 /** 32 * The name that will be used to separate the navigation cache within SESSION 33 */ 34 define('NAVIGATION_CACHE_NAME', 'navigation'); 35 define('NAVIGATION_SITE_ADMIN_CACHE_NAME', 'navigationsiteadmin'); 36 37 /** 38 * This class is used to represent a node in a navigation tree 39 * 40 * This class is used to represent a node in a navigation tree within Moodle, 41 * the tree could be one of global navigation, settings navigation, or the navbar. 42 * Each node can be one of two types either a Leaf (default) or a branch. 43 * When a node is first created it is created as a leaf, when/if children are added 44 * the node then becomes a branch. 45 * 46 * @package core 47 * @category navigation 48 * @copyright 2009 Sam Hemelryk 49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 50 */ 51 class navigation_node implements renderable { 52 /** @var int Used to identify this node a leaf (default) 0 */ 53 const NODETYPE_LEAF = 0; 54 /** @var int Used to identify this node a branch, happens with children 1 */ 55 const NODETYPE_BRANCH = 1; 56 /** @var null Unknown node type null */ 57 const TYPE_UNKNOWN = null; 58 /** @var int System node type 0 */ 59 const TYPE_ROOTNODE = 0; 60 /** @var int System node type 1 */ 61 const TYPE_SYSTEM = 1; 62 /** @var int Category node type 10 */ 63 const TYPE_CATEGORY = 10; 64 /** var int Category displayed in MyHome navigation node */ 65 const TYPE_MY_CATEGORY = 11; 66 /** @var int Course node type 20 */ 67 const TYPE_COURSE = 20; 68 /** @var int Course Structure node type 30 */ 69 const TYPE_SECTION = 30; 70 /** @var int Activity node type, e.g. Forum, Quiz 40 */ 71 const TYPE_ACTIVITY = 40; 72 /** @var int Resource node type, e.g. Link to a file, or label 50 */ 73 const TYPE_RESOURCE = 50; 74 /** @var int A custom node type, default when adding without specifing type 60 */ 75 const TYPE_CUSTOM = 60; 76 /** @var int Setting node type, used only within settings nav 70 */ 77 const TYPE_SETTING = 70; 78 /** @var int site admin branch node type, used only within settings nav 71 */ 79 const TYPE_SITE_ADMIN = 71; 80 /** @var int Setting node type, used only within settings nav 80 */ 81 const TYPE_USER = 80; 82 /** @var int Setting node type, used for containers of no importance 90 */ 83 const TYPE_CONTAINER = 90; 84 /** var int Course the current user is not enrolled in */ 85 const COURSE_OTHER = 0; 86 /** var int Course the current user is enrolled in but not viewing */ 87 const COURSE_MY = 1; 88 /** var int Course the current user is currently viewing */ 89 const COURSE_CURRENT = 2; 90 /** var string The course index page navigation node */ 91 const COURSE_INDEX_PAGE = 'courseindexpage'; 92 93 /** @var int Parameter to aid the coder in tracking [optional] */ 94 public $id = null; 95 /** @var string|int The identifier for the node, used to retrieve the node */ 96 public $key = null; 97 /** @var string|lang_string The text to use for the node */ 98 public $text = null; 99 /** @var string Short text to use if requested [optional] */ 100 public $shorttext = null; 101 /** @var string The title attribute for an action if one is defined */ 102 public $title = null; 103 /** @var string A string that can be used to build a help button */ 104 public $helpbutton = null; 105 /** @var moodle_url|action_link|null An action for the node (link) */ 106 public $action = null; 107 /** @var pix_icon The path to an icon to use for this node */ 108 public $icon = null; 109 /** @var int See TYPE_* constants defined for this class */ 110 public $type = self::TYPE_UNKNOWN; 111 /** @var int See NODETYPE_* constants defined for this class */ 112 public $nodetype = self::NODETYPE_LEAF; 113 /** @var bool If set to true the node will be collapsed by default */ 114 public $collapse = false; 115 /** @var bool If set to true the node will be expanded by default */ 116 public $forceopen = false; 117 /** @var array An array of CSS classes for the node */ 118 public $classes = array(); 119 /** @var navigation_node_collection An array of child nodes */ 120 public $children = array(); 121 /** @var bool If set to true the node will be recognised as active */ 122 public $isactive = false; 123 /** @var bool If set to true the node will be dimmed */ 124 public $hidden = false; 125 /** @var bool If set to false the node will not be displayed */ 126 public $display = true; 127 /** @var bool If set to true then an HR will be printed before the node */ 128 public $preceedwithhr = false; 129 /** @var bool If set to true the the navigation bar should ignore this node */ 130 public $mainnavonly = false; 131 /** @var bool If set to true a title will be added to the action no matter what */ 132 public $forcetitle = false; 133 /** @var navigation_node A reference to the node parent, you should never set this directly you should always call set_parent */ 134 public $parent = null; 135 /** @var bool Override to not display the icon even if one is provided **/ 136 public $hideicon = false; 137 /** @var bool Set to true if we KNOW that this node can be expanded. */ 138 public $isexpandable = false; 139 /** @var array */ 140 protected $namedtypes = array(0 => 'system', 10 => 'category', 20 => 'course', 30 => 'structure', 40 => 'activity', 141 50 => 'resource', 60 => 'custom', 70 => 'setting', 71 => 'siteadmin', 80 => 'user', 142 90 => 'container'); 143 /** @var moodle_url */ 144 protected static $fullmeurl = null; 145 /** @var bool toogles auto matching of active node */ 146 public static $autofindactive = true; 147 /** @var bool should we load full admin tree or rely on AJAX for performance reasons */ 148 protected static $loadadmintree = false; 149 /** @var mixed If set to an int, that section will be included even if it has no activities */ 150 public $includesectionnum = false; 151 /** @var bool does the node need to be loaded via ajax */ 152 public $requiresajaxloading = false; 153 /** @var bool If set to true this node will be added to the "flat" navigation */ 154 public $showinflatnavigation = false; 155 /** @var bool If set to true this node will be forced into a "more" menu whenever possible */ 156 public $forceintomoremenu = false; 157 /** @var bool If set to true this node will be displayed in the "secondary" navigation when applicable */ 158 public $showinsecondarynavigation = true; 159 /** @var bool If set to true the children of this node will be displayed within a submenu when applicable */ 160 public $showchildreninsubmenu = false; 161 /** @var string tab element ID. */ 162 public $tab; 163 /** @var string unique identifier. */ 164 public $moremenuid; 165 /** @var bool node that have children. */ 166 public $haschildren; 167 168 /** 169 * Constructs a new navigation_node 170 * 171 * @param array|string $properties Either an array of properties or a string to use 172 * as the text for the node 173 */ 174 public function __construct($properties) { 175 if (is_array($properties)) { 176 // Check the array for each property that we allow to set at construction. 177 // text - The main content for the node 178 // shorttext - A short text if required for the node 179 // icon - The icon to display for the node 180 // type - The type of the node 181 // key - The key to use to identify the node 182 // parent - A reference to the nodes parent 183 // action - The action to attribute to this node, usually a URL to link to 184 if (array_key_exists('text', $properties)) { 185 $this->text = $properties['text']; 186 } 187 if (array_key_exists('shorttext', $properties)) { 188 $this->shorttext = $properties['shorttext']; 189 } 190 if (!array_key_exists('icon', $properties)) { 191 $properties['icon'] = new pix_icon('i/navigationitem', ''); 192 } 193 $this->icon = $properties['icon']; 194 if ($this->icon instanceof pix_icon) { 195 if (empty($this->icon->attributes['class'])) { 196 $this->icon->attributes['class'] = 'navicon'; 197 } else { 198 $this->icon->attributes['class'] .= ' navicon'; 199 } 200 } 201 if (array_key_exists('type', $properties)) { 202 $this->type = $properties['type']; 203 } else { 204 $this->type = self::TYPE_CUSTOM; 205 } 206 if (array_key_exists('key', $properties)) { 207 $this->key = $properties['key']; 208 } 209 // This needs to happen last because of the check_if_active call that occurs 210 if (array_key_exists('action', $properties)) { 211 $this->action = $properties['action']; 212 if (is_string($this->action)) { 213 $this->action = new moodle_url($this->action); 214 } 215 if (self::$autofindactive) { 216 $this->check_if_active(); 217 } 218 } 219 if (array_key_exists('parent', $properties)) { 220 $this->set_parent($properties['parent']); 221 } 222 } else if (is_string($properties)) { 223 $this->text = $properties; 224 } 225 if ($this->text === null) { 226 throw new coding_exception('You must set the text for the node when you create it.'); 227 } 228 // Instantiate a new navigation node collection for this nodes children 229 $this->children = new navigation_node_collection(); 230 } 231 232 /** 233 * Checks if this node is the active node. 234 * 235 * This is determined by comparing the action for the node against the 236 * defined URL for the page. A match will see this node marked as active. 237 * 238 * @param int $strength One of URL_MATCH_EXACT, URL_MATCH_PARAMS, or URL_MATCH_BASE 239 * @return bool 240 */ 241 public function check_if_active($strength=URL_MATCH_EXACT) { 242 global $FULLME, $PAGE; 243 // Set fullmeurl if it hasn't already been set 244 if (self::$fullmeurl == null) { 245 if ($PAGE->has_set_url()) { 246 self::override_active_url(new moodle_url($PAGE->url)); 247 } else { 248 self::override_active_url(new moodle_url($FULLME)); 249 } 250 } 251 252 // Compare the action of this node against the fullmeurl 253 if ($this->action instanceof moodle_url && $this->action->compare(self::$fullmeurl, $strength)) { 254 $this->make_active(); 255 return true; 256 } 257 return false; 258 } 259 260 /** 261 * True if this nav node has siblings in the tree. 262 * 263 * @return bool 264 */ 265 public function has_siblings() { 266 if (empty($this->parent) || empty($this->parent->children)) { 267 return false; 268 } 269 if ($this->parent->children instanceof navigation_node_collection) { 270 $count = $this->parent->children->count(); 271 } else { 272 $count = count($this->parent->children); 273 } 274 return ($count > 1); 275 } 276 277 /** 278 * Get a list of sibling navigation nodes at the same level as this one. 279 * 280 * @return bool|array of navigation_node 281 */ 282 public function get_siblings() { 283 // Returns a list of the siblings of the current node for display in a flat navigation element. Either 284 // the in-page links or the breadcrumb links. 285 $siblings = false; 286 287 if ($this->has_siblings()) { 288 $siblings = []; 289 foreach ($this->parent->children as $child) { 290 if ($child->display) { 291 $siblings[] = $child; 292 } 293 } 294 } 295 return $siblings; 296 } 297 298 /** 299 * This sets the URL that the URL of new nodes get compared to when locating 300 * the active node. 301 * 302 * The active node is the node that matches the URL set here. By default this 303 * is either $PAGE->url or if that hasn't been set $FULLME. 304 * 305 * @param moodle_url $url The url to use for the fullmeurl. 306 * @param bool $loadadmintree use true if the URL point to administration tree 307 */ 308 public static function override_active_url(moodle_url $url, $loadadmintree = false) { 309 // Clone the URL, in case the calling script changes their URL later. 310 self::$fullmeurl = new moodle_url($url); 311 // True means we do not want AJAX loaded admin tree, required for all admin pages. 312 if ($loadadmintree) { 313 // Do not change back to false if already set. 314 self::$loadadmintree = true; 315 } 316 } 317 318 /** 319 * Use when page is linked from the admin tree, 320 * if not used navigation could not find the page using current URL 321 * because the tree is not fully loaded. 322 */ 323 public static function require_admin_tree() { 324 self::$loadadmintree = true; 325 } 326 327 /** 328 * Creates a navigation node, ready to add it as a child using add_node 329 * function. (The created node needs to be added before you can use it.) 330 * @param string $text 331 * @param moodle_url|action_link $action 332 * @param int $type 333 * @param string $shorttext 334 * @param string|int $key 335 * @param pix_icon $icon 336 * @return navigation_node 337 */ 338 public static function create($text, $action=null, $type=self::TYPE_CUSTOM, 339 $shorttext=null, $key=null, pix_icon $icon=null) { 340 if ($action && !($action instanceof moodle_url || $action instanceof action_link)) { 341 debugging( 342 "It is required that the action provided be either an action_url|moodle_url." . 343 " Please update your definition.", E_NOTICE); 344 } 345 // Properties array used when creating the new navigation node 346 $itemarray = array( 347 'text' => $text, 348 'type' => $type 349 ); 350 // Set the action if one was provided 351 if ($action!==null) { 352 $itemarray['action'] = $action; 353 } 354 // Set the shorttext if one was provided 355 if ($shorttext!==null) { 356 $itemarray['shorttext'] = $shorttext; 357 } 358 // Set the icon if one was provided 359 if ($icon!==null) { 360 $itemarray['icon'] = $icon; 361 } 362 // Set the key 363 $itemarray['key'] = $key; 364 // Construct and return 365 return new navigation_node($itemarray); 366 } 367 368 /** 369 * Adds a navigation node as a child of this node. 370 * 371 * @param string $text 372 * @param moodle_url|action_link|string $action 373 * @param int $type 374 * @param string $shorttext 375 * @param string|int $key 376 * @param pix_icon $icon 377 * @return navigation_node 378 */ 379 public function add($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) { 380 if ($action && is_string($action)) { 381 $action = new moodle_url($action); 382 } 383 // Create child node 384 $childnode = self::create($text, $action, $type, $shorttext, $key, $icon); 385 386 // Add the child to end and return 387 return $this->add_node($childnode); 388 } 389 390 /** 391 * Adds a navigation node as a child of this one, given a $node object 392 * created using the create function. 393 * @param navigation_node $childnode Node to add 394 * @param string $beforekey 395 * @return navigation_node The added node 396 */ 397 public function add_node(navigation_node $childnode, $beforekey=null) { 398 // First convert the nodetype for this node to a branch as it will now have children 399 if ($this->nodetype !== self::NODETYPE_BRANCH) { 400 $this->nodetype = self::NODETYPE_BRANCH; 401 } 402 // Set the parent to this node 403 $childnode->set_parent($this); 404 405 // Default the key to the number of children if not provided 406 if ($childnode->key === null) { 407 $childnode->key = $this->children->count(); 408 } 409 410 // Add the child using the navigation_node_collections add method 411 $node = $this->children->add($childnode, $beforekey); 412 413 // If added node is a category node or the user is logged in and it's a course 414 // then mark added node as a branch (makes it expandable by AJAX) 415 $type = $childnode->type; 416 if (($type == self::TYPE_CATEGORY) || (isloggedin() && ($type == self::TYPE_COURSE)) || ($type == self::TYPE_MY_CATEGORY) || 417 ($type === self::TYPE_SITE_ADMIN)) { 418 $node->nodetype = self::NODETYPE_BRANCH; 419 } 420 // If this node is hidden mark it's children as hidden also 421 if ($this->hidden) { 422 $node->hidden = true; 423 } 424 // Return added node (reference returned by $this->children->add() 425 return $node; 426 } 427 428 /** 429 * Return a list of all the keys of all the child nodes. 430 * @return array the keys. 431 */ 432 public function get_children_key_list() { 433 return $this->children->get_key_list(); 434 } 435 436 /** 437 * Searches for a node of the given type with the given key. 438 * 439 * This searches this node plus all of its children, and their children.... 440 * If you know the node you are looking for is a child of this node then please 441 * use the get method instead. 442 * 443 * @param int|string $key The key of the node we are looking for 444 * @param int $type One of navigation_node::TYPE_* 445 * @return navigation_node|false 446 */ 447 public function find($key, $type) { 448 return $this->children->find($key, $type); 449 } 450 451 /** 452 * Walk the tree building up a list of all the flat navigation nodes. 453 * 454 * @deprecated since Moodle 4.0 455 * @param flat_navigation $nodes List of the found flat navigation nodes. 456 * @param boolean $showdivider Show a divider before the first node. 457 * @param string $label A label for the collection of navigation links. 458 */ 459 public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false, $label = '') { 460 debugging("Function has been deprecated with the deprecation of the flat_navigation class."); 461 if ($this->showinflatnavigation) { 462 $indent = 0; 463 if ($this->type == self::TYPE_COURSE || $this->key === self::COURSE_INDEX_PAGE) { 464 $indent = 1; 465 } 466 $flat = new flat_navigation_node($this, $indent); 467 $flat->set_showdivider($showdivider, $label); 468 $nodes->add($flat); 469 } 470 foreach ($this->children as $child) { 471 $child->build_flat_navigation_list($nodes, false); 472 } 473 } 474 475 /** 476 * Get the child of this node that has the given key + (optional) type. 477 * 478 * If you are looking for a node and want to search all children + their children 479 * then please use the find method instead. 480 * 481 * @param int|string $key The key of the node we are looking for 482 * @param int $type One of navigation_node::TYPE_* 483 * @return navigation_node|false 484 */ 485 public function get($key, $type=null) { 486 return $this->children->get($key, $type); 487 } 488 489 /** 490 * Removes this node. 491 * 492 * @return bool 493 */ 494 public function remove() { 495 return $this->parent->children->remove($this->key, $this->type); 496 } 497 498 /** 499 * Checks if this node has or could have any children 500 * 501 * @return bool Returns true if it has children or could have (by AJAX expansion) 502 */ 503 public function has_children() { 504 return ($this->nodetype === navigation_node::NODETYPE_BRANCH || $this->children->count()>0 || $this->isexpandable); 505 } 506 507 /** 508 * Marks this node as active and forces it open. 509 * 510 * Important: If you are here because you need to mark a node active to get 511 * the navigation to do what you want have you looked at {@link navigation_node::override_active_url()}? 512 * You can use it to specify a different URL to match the active navigation node on 513 * rather than having to locate and manually mark a node active. 514 */ 515 public function make_active() { 516 $this->isactive = true; 517 $this->add_class('active_tree_node'); 518 $this->force_open(); 519 if ($this->parent !== null) { 520 $this->parent->make_inactive(); 521 } 522 } 523 524 /** 525 * Marks a node as inactive and recusised back to the base of the tree 526 * doing the same to all parents. 527 */ 528 public function make_inactive() { 529 $this->isactive = false; 530 $this->remove_class('active_tree_node'); 531 if ($this->parent !== null) { 532 $this->parent->make_inactive(); 533 } 534 } 535 536 /** 537 * Forces this node to be open and at the same time forces open all 538 * parents until the root node. 539 * 540 * Recursive. 541 */ 542 public function force_open() { 543 $this->forceopen = true; 544 if ($this->parent !== null) { 545 $this->parent->force_open(); 546 } 547 } 548 549 /** 550 * Adds a CSS class to this node. 551 * 552 * @param string $class 553 * @return bool 554 */ 555 public function add_class($class) { 556 if (!in_array($class, $this->classes)) { 557 $this->classes[] = $class; 558 } 559 return true; 560 } 561 562 /** 563 * Removes a CSS class from this node. 564 * 565 * @param string $class 566 * @return bool True if the class was successfully removed. 567 */ 568 public function remove_class($class) { 569 if (in_array($class, $this->classes)) { 570 $key = array_search($class,$this->classes); 571 if ($key!==false) { 572 // Remove the class' array element. 573 unset($this->classes[$key]); 574 // Reindex the array to avoid failures when the classes array is iterated later in mustache templates. 575 $this->classes = array_values($this->classes); 576 577 return true; 578 } 579 } 580 return false; 581 } 582 583 /** 584 * Sets the title for this node and forces Moodle to utilise it. 585 * 586 * Note that this method is named identically to the public "title" property of the class, which unfortunately confuses 587 * our Mustache renderer, because it will see the method and try and call it without any arguments (hence must be nullable) 588 * before trying to access the public property 589 * 590 * @param string|null $title 591 * @return string 592 */ 593 public function title(?string $title = null): string { 594 if ($title !== null) { 595 $this->title = $title; 596 $this->forcetitle = true; 597 } 598 return (string) $this->title; 599 } 600 601 /** 602 * Resets the page specific information on this node if it is being unserialised. 603 */ 604 public function __wakeup(){ 605 $this->forceopen = false; 606 $this->isactive = false; 607 $this->remove_class('active_tree_node'); 608 } 609 610 /** 611 * Checks if this node or any of its children contain the active node. 612 * 613 * Recursive. 614 * 615 * @return bool 616 */ 617 public function contains_active_node() { 618 if ($this->isactive) { 619 return true; 620 } else { 621 foreach ($this->children as $child) { 622 if ($child->isactive || $child->contains_active_node()) { 623 return true; 624 } 625 } 626 } 627 return false; 628 } 629 630 /** 631 * To better balance the admin tree, we want to group all the short top branches together. 632 * 633 * This means < 8 nodes and no subtrees. 634 * 635 * @return bool 636 */ 637 public function is_short_branch() { 638 $limit = 8; 639 if ($this->children->count() >= $limit) { 640 return false; 641 } 642 foreach ($this->children as $child) { 643 if ($child->has_children()) { 644 return false; 645 } 646 } 647 return true; 648 } 649 650 /** 651 * Finds the active node. 652 * 653 * Searches this nodes children plus all of the children for the active node 654 * and returns it if found. 655 * 656 * Recursive. 657 * 658 * @return navigation_node|false 659 */ 660 public function find_active_node() { 661 if ($this->isactive) { 662 return $this; 663 } else { 664 foreach ($this->children as &$child) { 665 $outcome = $child->find_active_node(); 666 if ($outcome !== false) { 667 return $outcome; 668 } 669 } 670 } 671 return false; 672 } 673 674 /** 675 * Searches all children for the best matching active node 676 * @param int $strength The url match to be made. 677 * @return navigation_node|false 678 */ 679 public function search_for_active_node($strength = URL_MATCH_BASE) { 680 if ($this->check_if_active($strength)) { 681 return $this; 682 } else { 683 foreach ($this->children as &$child) { 684 $outcome = $child->search_for_active_node($strength); 685 if ($outcome !== false) { 686 return $outcome; 687 } 688 } 689 } 690 return false; 691 } 692 693 /** 694 * Gets the content for this node. 695 * 696 * @param bool $shorttext If true shorttext is used rather than the normal text 697 * @return string 698 */ 699 public function get_content($shorttext=false) { 700 $navcontext = \context_helper::get_navigation_filter_context(null); 701 $options = !empty($navcontext) ? ['context' => $navcontext] : null; 702 703 if ($shorttext && $this->shorttext!==null) { 704 return format_string($this->shorttext, null, $options); 705 } else { 706 return format_string($this->text, null, $options); 707 } 708 } 709 710 /** 711 * Gets the title to use for this node. 712 * 713 * @return string 714 */ 715 public function get_title() { 716 if ($this->forcetitle || $this->action != null){ 717 return $this->title; 718 } else { 719 return ''; 720 } 721 } 722 723 /** 724 * Used to easily determine if this link in the breadcrumbs has a valid action/url. 725 * 726 * @return boolean 727 */ 728 public function has_action() { 729 return !empty($this->action); 730 } 731 732 /** 733 * Used to easily determine if the action is an internal link. 734 * 735 * @return bool 736 */ 737 public function has_internal_action(): bool { 738 global $CFG; 739 if ($this->has_action()) { 740 $url = $this->action(); 741 if ($this->action() instanceof \action_link) { 742 $url = $this->action()->url; 743 } 744 745 if (($url->out() === $CFG->wwwroot) || (strpos($url->out(), $CFG->wwwroot.'/') === 0)) { 746 return true; 747 } 748 } 749 return false; 750 } 751 752 /** 753 * Used to easily determine if this link in the breadcrumbs is hidden. 754 * 755 * @return boolean 756 */ 757 public function is_hidden() { 758 return $this->hidden; 759 } 760 761 /** 762 * Gets the CSS class to add to this node to describe its type 763 * 764 * @return string 765 */ 766 public function get_css_type() { 767 if (array_key_exists($this->type, $this->namedtypes)) { 768 return 'type_'.$this->namedtypes[$this->type]; 769 } 770 return 'type_unknown'; 771 } 772 773 /** 774 * Finds all nodes that are expandable by AJAX 775 * 776 * @param array $expandable An array by reference to populate with expandable nodes. 777 */ 778 public function find_expandable(array &$expandable) { 779 foreach ($this->children as &$child) { 780 if ($child->display && $child->has_children() && $child->children->count() == 0) { 781 $child->id = 'expandable_branch_'.$child->type.'_'.clean_param($child->key, PARAM_ALPHANUMEXT); 782 $this->add_class('canexpand'); 783 $child->requiresajaxloading = true; 784 $expandable[] = array('id' => $child->id, 'key' => $child->key, 'type' => $child->type); 785 } 786 $child->find_expandable($expandable); 787 } 788 } 789 790 /** 791 * Finds all nodes of a given type (recursive) 792 * 793 * @param int $type One of navigation_node::TYPE_* 794 * @return array 795 */ 796 public function find_all_of_type($type) { 797 $nodes = $this->children->type($type); 798 foreach ($this->children as &$node) { 799 $childnodes = $node->find_all_of_type($type); 800 $nodes = array_merge($nodes, $childnodes); 801 } 802 return $nodes; 803 } 804 805 /** 806 * Removes this node if it is empty 807 */ 808 public function trim_if_empty() { 809 if ($this->children->count() == 0) { 810 $this->remove(); 811 } 812 } 813 814 /** 815 * Creates a tab representation of this nodes children that can be used 816 * with print_tabs to produce the tabs on a page. 817 * 818 * call_user_func_array('print_tabs', $node->get_tabs_array()); 819 * 820 * @param array $inactive 821 * @param bool $return 822 * @return array Array (tabs, selected, inactive, activated, return) 823 */ 824 public function get_tabs_array(array $inactive=array(), $return=false) { 825 $tabs = array(); 826 $rows = array(); 827 $selected = null; 828 $activated = array(); 829 foreach ($this->children as $node) { 830 $tabs[] = new tabobject($node->key, $node->action, $node->get_content(), $node->get_title()); 831 if ($node->contains_active_node()) { 832 if ($node->children->count() > 0) { 833 $activated[] = $node->key; 834 foreach ($node->children as $child) { 835 if ($child->contains_active_node()) { 836 $selected = $child->key; 837 } 838 $rows[] = new tabobject($child->key, $child->action, $child->get_content(), $child->get_title()); 839 } 840 } else { 841 $selected = $node->key; 842 } 843 } 844 } 845 return array(array($tabs, $rows), $selected, $inactive, $activated, $return); 846 } 847 848 /** 849 * Sets the parent for this node and if this node is active ensures that the tree is properly 850 * adjusted as well. 851 * 852 * @param navigation_node $parent 853 */ 854 public function set_parent(navigation_node $parent) { 855 // Set the parent (thats the easy part) 856 $this->parent = $parent; 857 // Check if this node is active (this is checked during construction) 858 if ($this->isactive) { 859 // Force all of the parent nodes open so you can see this node 860 $this->parent->force_open(); 861 // Make all parents inactive so that its clear where we are. 862 $this->parent->make_inactive(); 863 } 864 } 865 866 /** 867 * Hides the node and any children it has. 868 * 869 * @since Moodle 2.5 870 * @param array $typestohide Optional. An array of node types that should be hidden. 871 * If null all nodes will be hidden. 872 * If an array is given then nodes will only be hidden if their type mtatches an element in the array. 873 * e.g. array(navigation_node::TYPE_COURSE) would hide only course nodes. 874 */ 875 public function hide(array $typestohide = null) { 876 if ($typestohide === null || in_array($this->type, $typestohide)) { 877 $this->display = false; 878 if ($this->has_children()) { 879 foreach ($this->children as $child) { 880 $child->hide($typestohide); 881 } 882 } 883 } 884 } 885 886 /** 887 * Get the action url for this navigation node. 888 * Called from templates. 889 * 890 * @since Moodle 3.2 891 */ 892 public function action() { 893 if ($this->action instanceof moodle_url) { 894 return $this->action; 895 } else if ($this->action instanceof action_link) { 896 return $this->action->url; 897 } 898 return $this->action; 899 } 900 901 /** 902 * Return an array consisting of the additional attributes for the action url. 903 * 904 * @return array Formatted array to parse in a template 905 */ 906 public function actionattributes() { 907 if ($this->action instanceof action_link) { 908 return array_map(function($key, $value) { 909 return [ 910 'name' => $key, 911 'value' => $value 912 ]; 913 }, array_keys($this->action->attributes), $this->action->attributes); 914 } 915 916 return []; 917 } 918 919 /** 920 * Check whether the node's action is of type action_link. 921 * 922 * @return bool 923 */ 924 public function is_action_link() { 925 return $this->action instanceof action_link; 926 } 927 928 /** 929 * Return an array consisting of the actions for the action link. 930 * 931 * @return array Formatted array to parse in a template 932 */ 933 public function action_link_actions() { 934 global $PAGE; 935 936 if (!$this->is_action_link()) { 937 return []; 938 } 939 940 $actionid = $this->action->attributes['id']; 941 $actionsdata = array_map(function($action) use ($PAGE, $actionid) { 942 $data = $action->export_for_template($PAGE->get_renderer('core')); 943 $data->id = $actionid; 944 return $data; 945 }, !empty($this->action->actions) ? $this->action->actions : []); 946 947 return ['actions' => $actionsdata]; 948 } 949 950 /** 951 * Sets whether the node and its children should be added into a "more" menu whenever possible. 952 * 953 * @param bool $forceintomoremenu 954 */ 955 public function set_force_into_more_menu(bool $forceintomoremenu = false) { 956 $this->forceintomoremenu = $forceintomoremenu; 957 foreach ($this->children as $child) { 958 $child->set_force_into_more_menu($forceintomoremenu); 959 } 960 } 961 962 /** 963 * Sets whether the node and its children should be displayed in the "secondary" navigation when applicable. 964 * 965 * @param bool $show 966 */ 967 public function set_show_in_secondary_navigation(bool $show = true) { 968 $this->showinsecondarynavigation = $show; 969 foreach ($this->children as $child) { 970 $child->set_show_in_secondary_navigation($show); 971 } 972 } 973 974 /** 975 * Add the menu item to handle locking and unlocking of a conext. 976 * 977 * @param \navigation_node $node Node to add 978 * @param \context $context The context to be locked 979 */ 980 protected function add_context_locking_node(\navigation_node $node, \context $context) { 981 global $CFG; 982 // Manage context locking. 983 if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $context)) { 984 $parentcontext = $context->get_parent_context(); 985 if (empty($parentcontext) || !$parentcontext->locked) { 986 if ($context->locked) { 987 $lockicon = 'i/unlock'; 988 $lockstring = get_string('managecontextunlock', 'admin'); 989 } else { 990 $lockicon = 'i/lock'; 991 $lockstring = get_string('managecontextlock', 'admin'); 992 } 993 $node->add( 994 $lockstring, 995 new moodle_url( 996 '/admin/lock.php', 997 [ 998 'id' => $context->id, 999 ] 1000 ), 1001 self::TYPE_SETTING, 1002 null, 1003 'contextlocking', 1004 new pix_icon($lockicon, '') 1005 ); 1006 } 1007 } 1008 1009 } 1010 } 1011 1012 /** 1013 * Navigation node collection 1014 * 1015 * This class is responsible for managing a collection of navigation nodes. 1016 * It is required because a node's unique identifier is a combination of both its 1017 * key and its type. 1018 * 1019 * Originally an array was used with a string key that was a combination of the two 1020 * however it was decided that a better solution would be to use a class that 1021 * implements the standard IteratorAggregate interface. 1022 * 1023 * @package core 1024 * @category navigation 1025 * @copyright 2010 Sam Hemelryk 1026 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1027 */ 1028 class navigation_node_collection implements IteratorAggregate, Countable { 1029 /** 1030 * A multidimensional array to where the first key is the type and the second 1031 * key is the nodes key. 1032 * @var array 1033 */ 1034 protected $collection = array(); 1035 /** 1036 * An array that contains references to nodes in the same order they were added. 1037 * This is maintained as a progressive array. 1038 * @var array 1039 */ 1040 protected $orderedcollection = array(); 1041 /** 1042 * A reference to the last node that was added to the collection 1043 * @var navigation_node 1044 */ 1045 protected $last = null; 1046 /** 1047 * The total number of items added to this array. 1048 * @var int 1049 */ 1050 protected $count = 0; 1051 1052 /** 1053 * Label for collection of nodes. 1054 * @var string 1055 */ 1056 protected $collectionlabel = ''; 1057 1058 /** 1059 * Adds a navigation node to the collection 1060 * 1061 * @param navigation_node $node Node to add 1062 * @param string $beforekey If specified, adds before a node with this key, 1063 * otherwise adds at end 1064 * @return navigation_node Added node 1065 */ 1066 public function add(navigation_node $node, $beforekey=null) { 1067 global $CFG; 1068 $key = $node->key; 1069 $type = $node->type; 1070 1071 // First check we have a 2nd dimension for this type 1072 if (!array_key_exists($type, $this->orderedcollection)) { 1073 $this->orderedcollection[$type] = array(); 1074 } 1075 // Check for a collision and report if debugging is turned on 1076 if ($CFG->debug && array_key_exists($key, $this->orderedcollection[$type])) { 1077 debugging('Navigation node intersect: Adding a node that already exists '.$key, DEBUG_DEVELOPER); 1078 } 1079 1080 // Find the key to add before 1081 $newindex = $this->count; 1082 $last = true; 1083 if ($beforekey !== null) { 1084 foreach ($this->collection as $index => $othernode) { 1085 if ($othernode->key === $beforekey) { 1086 $newindex = $index; 1087 $last = false; 1088 break; 1089 } 1090 } 1091 if ($newindex === $this->count) { 1092 debugging('Navigation node add_before: Reference node not found ' . $beforekey . 1093 ', options: ' . implode(' ', $this->get_key_list()), DEBUG_DEVELOPER); 1094 } 1095 } 1096 1097 // Add the node to the appropriate place in the by-type structure (which 1098 // is not ordered, despite the variable name) 1099 $this->orderedcollection[$type][$key] = $node; 1100 if (!$last) { 1101 // Update existing references in the ordered collection (which is the 1102 // one that isn't called 'ordered') to shuffle them along if required 1103 for ($oldindex = $this->count; $oldindex > $newindex; $oldindex--) { 1104 $this->collection[$oldindex] = $this->collection[$oldindex - 1]; 1105 } 1106 } 1107 // Add a reference to the node to the progressive collection. 1108 $this->collection[$newindex] = $this->orderedcollection[$type][$key]; 1109 // Update the last property to a reference to this new node. 1110 $this->last = $this->orderedcollection[$type][$key]; 1111 1112 // Reorder the array by index if needed 1113 if (!$last) { 1114 ksort($this->collection); 1115 } 1116 $this->count++; 1117 // Return the reference to the now added node 1118 return $node; 1119 } 1120 1121 /** 1122 * Return a list of all the keys of all the nodes. 1123 * @return array the keys. 1124 */ 1125 public function get_key_list() { 1126 $keys = array(); 1127 foreach ($this->collection as $node) { 1128 $keys[] = $node->key; 1129 } 1130 return $keys; 1131 } 1132 1133 /** 1134 * Set a label for this collection. 1135 * 1136 * @param string $label 1137 */ 1138 public function set_collectionlabel($label) { 1139 $this->collectionlabel = $label; 1140 } 1141 1142 /** 1143 * Return a label for this collection. 1144 * 1145 * @return string 1146 */ 1147 public function get_collectionlabel() { 1148 return $this->collectionlabel; 1149 } 1150 1151 /** 1152 * Fetches a node from this collection. 1153 * 1154 * @param string|int $key The key of the node we want to find. 1155 * @param int $type One of navigation_node::TYPE_*. 1156 * @return navigation_node|null 1157 */ 1158 public function get($key, $type=null) { 1159 if ($type !== null) { 1160 // If the type is known then we can simply check and fetch 1161 if (!empty($this->orderedcollection[$type][$key])) { 1162 return $this->orderedcollection[$type][$key]; 1163 } 1164 } else { 1165 // Because we don't know the type we look in the progressive array 1166 foreach ($this->collection as $node) { 1167 if ($node->key === $key) { 1168 return $node; 1169 } 1170 } 1171 } 1172 return false; 1173 } 1174 1175 /** 1176 * Searches for a node with matching key and type. 1177 * 1178 * This function searches both the nodes in this collection and all of 1179 * the nodes in each collection belonging to the nodes in this collection. 1180 * 1181 * Recursive. 1182 * 1183 * @param string|int $key The key of the node we want to find. 1184 * @param int $type One of navigation_node::TYPE_*. 1185 * @return navigation_node|false 1186 */ 1187 public function find($key, $type=null) { 1188 if ($type !== null && array_key_exists($type, $this->orderedcollection) && array_key_exists($key, $this->orderedcollection[$type])) { 1189 return $this->orderedcollection[$type][$key]; 1190 } else { 1191 $nodes = $this->getIterator(); 1192 // Search immediate children first 1193 foreach ($nodes as &$node) { 1194 if ($node->key === $key && ($type === null || $type === $node->type)) { 1195 return $node; 1196 } 1197 } 1198 // Now search each childs children 1199 foreach ($nodes as &$node) { 1200 $result = $node->children->find($key, $type); 1201 if ($result !== false) { 1202 return $result; 1203 } 1204 } 1205 } 1206 return false; 1207 } 1208 1209 /** 1210 * Fetches the last node that was added to this collection 1211 * 1212 * @return navigation_node 1213 */ 1214 public function last() { 1215 return $this->last; 1216 } 1217 1218 /** 1219 * Fetches all nodes of a given type from this collection 1220 * 1221 * @param string|int $type node type being searched for. 1222 * @return array ordered collection 1223 */ 1224 public function type($type) { 1225 if (!array_key_exists($type, $this->orderedcollection)) { 1226 $this->orderedcollection[$type] = array(); 1227 } 1228 return $this->orderedcollection[$type]; 1229 } 1230 /** 1231 * Removes the node with the given key and type from the collection 1232 * 1233 * @param string|int $key The key of the node we want to find. 1234 * @param int $type 1235 * @return bool 1236 */ 1237 public function remove($key, $type=null) { 1238 $child = $this->get($key, $type); 1239 if ($child !== false) { 1240 foreach ($this->collection as $colkey => $node) { 1241 if ($node->key === $key && (is_null($type) || $node->type == $type)) { 1242 unset($this->collection[$colkey]); 1243 $this->collection = array_values($this->collection); 1244 break; 1245 } 1246 } 1247 unset($this->orderedcollection[$child->type][$child->key]); 1248 $this->count--; 1249 return true; 1250 } 1251 return false; 1252 } 1253 1254 /** 1255 * Gets the number of nodes in this collection 1256 * 1257 * This option uses an internal count rather than counting the actual options to avoid 1258 * a performance hit through the count function. 1259 * 1260 * @return int 1261 */ 1262 public function count(): int { 1263 return $this->count; 1264 } 1265 /** 1266 * Gets an array iterator for the collection. 1267 * 1268 * This is required by the IteratorAggregator interface and is used by routines 1269 * such as the foreach loop. 1270 * 1271 * @return ArrayIterator 1272 */ 1273 public function getIterator(): Traversable { 1274 return new ArrayIterator($this->collection); 1275 } 1276 } 1277 1278 /** 1279 * The global navigation class used for... the global navigation 1280 * 1281 * This class is used by PAGE to store the global navigation for the site 1282 * and is then used by the settings nav and navbar to save on processing and DB calls 1283 * 1284 * See 1285 * {@link lib/pagelib.php} {@link moodle_page::initialise_theme_and_output()} 1286 * {@link lib/ajax/getnavbranch.php} Called by ajax 1287 * 1288 * @package core 1289 * @category navigation 1290 * @copyright 2009 Sam Hemelryk 1291 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1292 */ 1293 class global_navigation extends navigation_node { 1294 /** @var moodle_page The Moodle page this navigation object belongs to. */ 1295 protected $page; 1296 /** @var bool switch to let us know if the navigation object is initialised*/ 1297 protected $initialised = false; 1298 /** @var array An array of course information */ 1299 protected $mycourses = array(); 1300 /** @var navigation_node[] An array for containing root navigation nodes */ 1301 protected $rootnodes = array(); 1302 /** @var bool A switch for whether to show empty sections in the navigation */ 1303 protected $showemptysections = true; 1304 /** @var bool A switch for whether courses should be shown within categories on the navigation. */ 1305 protected $showcategories = null; 1306 /** @var null@var bool A switch for whether or not to show categories in the my courses branch. */ 1307 protected $showmycategories = null; 1308 /** @var array An array of stdClasses for users that the navigation is extended for */ 1309 protected $extendforuser = array(); 1310 /** @var navigation_cache */ 1311 protected $cache; 1312 /** @var array An array of course ids that are present in the navigation */ 1313 protected $addedcourses = array(); 1314 /** @var bool */ 1315 protected $allcategoriesloaded = false; 1316 /** @var array An array of category ids that are included in the navigation */ 1317 protected $addedcategories = array(); 1318 /** @var int expansion limit */ 1319 protected $expansionlimit = 0; 1320 /** @var int userid to allow parent to see child's profile page navigation */ 1321 protected $useridtouseforparentchecks = 0; 1322 /** @var cache_session A cache that stores information on expanded courses */ 1323 protected $cacheexpandcourse = null; 1324 1325 /** Used when loading categories to load all top level categories [parent = 0] **/ 1326 const LOAD_ROOT_CATEGORIES = 0; 1327 /** Used when loading categories to load all categories **/ 1328 const LOAD_ALL_CATEGORIES = -1; 1329 1330 /** 1331 * Constructs a new global navigation 1332 * 1333 * @param moodle_page $page The page this navigation object belongs to 1334 */ 1335 public function __construct(moodle_page $page) { 1336 global $CFG, $SITE, $USER; 1337 1338 if (during_initial_install()) { 1339 return; 1340 } 1341 1342 $homepage = get_home_page(); 1343 if ($homepage == HOMEPAGE_SITE) { 1344 // We are using the site home for the root element. 1345 $properties = array( 1346 'key' => 'home', 1347 'type' => navigation_node::TYPE_SYSTEM, 1348 'text' => get_string('home'), 1349 'action' => new moodle_url('/'), 1350 'icon' => new pix_icon('i/home', '') 1351 ); 1352 } else if ($homepage == HOMEPAGE_MYCOURSES) { 1353 // We are using the user's course summary page for the root element. 1354 $properties = array( 1355 'key' => 'mycourses', 1356 'type' => navigation_node::TYPE_SYSTEM, 1357 'text' => get_string('mycourses'), 1358 'action' => new moodle_url('/my/courses.php'), 1359 'icon' => new pix_icon('i/course', '') 1360 ); 1361 } else { 1362 // We are using the users my moodle for the root element. 1363 $properties = array( 1364 'key' => 'myhome', 1365 'type' => navigation_node::TYPE_SYSTEM, 1366 'text' => get_string('myhome'), 1367 'action' => new moodle_url('/my/'), 1368 'icon' => new pix_icon('i/dashboard', '') 1369 ); 1370 } 1371 1372 // Use the parents constructor.... good good reuse 1373 parent::__construct($properties); 1374 $this->showinflatnavigation = true; 1375 1376 // Initalise and set defaults 1377 $this->page = $page; 1378 $this->forceopen = true; 1379 $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME); 1380 } 1381 1382 /** 1383 * Mutator to set userid to allow parent to see child's profile 1384 * page navigation. See MDL-25805 for initial issue. Linked to it 1385 * is an issue explaining why this is a REALLY UGLY HACK thats not 1386 * for you to use! 1387 * 1388 * @param int $userid userid of profile page that parent wants to navigate around. 1389 */ 1390 public function set_userid_for_parent_checks($userid) { 1391 $this->useridtouseforparentchecks = $userid; 1392 } 1393 1394 1395 /** 1396 * Initialises the navigation object. 1397 * 1398 * This causes the navigation object to look at the current state of the page 1399 * that it is associated with and then load the appropriate content. 1400 * 1401 * This should only occur the first time that the navigation structure is utilised 1402 * which will normally be either when the navbar is called to be displayed or 1403 * when a block makes use of it. 1404 * 1405 * @return bool 1406 */ 1407 public function initialise() { 1408 global $CFG, $SITE, $USER; 1409 // Check if it has already been initialised 1410 if ($this->initialised || during_initial_install()) { 1411 return true; 1412 } 1413 $this->initialised = true; 1414 1415 // Set up the five base root nodes. These are nodes where we will put our 1416 // content and are as follows: 1417 // site: Navigation for the front page. 1418 // myprofile: User profile information goes here. 1419 // currentcourse: The course being currently viewed. 1420 // mycourses: The users courses get added here. 1421 // courses: Additional courses are added here. 1422 // users: Other users information loaded here. 1423 $this->rootnodes = array(); 1424 $defaulthomepage = get_home_page(); 1425 if ($defaulthomepage == HOMEPAGE_SITE) { 1426 // The home element should be my moodle because the root element is the site 1427 if (isloggedin() && !isguestuser()) { // Makes no sense if you aren't logged in 1428 if (!empty($CFG->enabledashboard)) { 1429 // Only add dashboard to home if it's enabled. 1430 $this->rootnodes['home'] = $this->add(get_string('myhome'), new moodle_url('/my/'), 1431 self::TYPE_SETTING, null, 'myhome', new pix_icon('i/dashboard', '')); 1432 $this->rootnodes['home']->showinflatnavigation = true; 1433 } 1434 } 1435 } else { 1436 // The home element should be the site because the root node is my moodle 1437 $this->rootnodes['home'] = $this->add(get_string('sitehome'), new moodle_url('/'), 1438 self::TYPE_SETTING, null, 'home', new pix_icon('i/home', '')); 1439 $this->rootnodes['home']->showinflatnavigation = true; 1440 if (!empty($CFG->defaulthomepage) && 1441 ($CFG->defaulthomepage == HOMEPAGE_MY || $CFG->defaulthomepage == HOMEPAGE_MYCOURSES)) { 1442 // We need to stop automatic redirection 1443 $this->rootnodes['home']->action->param('redirect', '0'); 1444 } 1445 } 1446 $this->rootnodes['site'] = $this->add_course($SITE); 1447 $this->rootnodes['myprofile'] = $this->add(get_string('profile'), null, self::TYPE_USER, null, 'myprofile'); 1448 $this->rootnodes['currentcourse'] = $this->add(get_string('currentcourse'), null, self::TYPE_ROOTNODE, null, 'currentcourse'); 1449 $this->rootnodes['mycourses'] = $this->add( 1450 get_string('mycourses'), 1451 new moodle_url('/my/courses.php'), 1452 self::TYPE_ROOTNODE, 1453 null, 1454 'mycourses', 1455 new pix_icon('i/course', '') 1456 ); 1457 // We do not need to show this node in the breadcrumbs if the default homepage is mycourses. 1458 // It will be automatically handled by the breadcrumb generator. 1459 if ($defaulthomepage == HOMEPAGE_MYCOURSES) { 1460 $this->rootnodes['mycourses']->mainnavonly = true; 1461 } 1462 1463 $this->rootnodes['courses'] = $this->add(get_string('courses'), new moodle_url('/course/index.php'), self::TYPE_ROOTNODE, null, 'courses'); 1464 if (!core_course_category::user_top()) { 1465 $this->rootnodes['courses']->hide(); 1466 } 1467 $this->rootnodes['users'] = $this->add(get_string('users'), null, self::TYPE_ROOTNODE, null, 'users'); 1468 1469 // We always load the frontpage course to ensure it is available without 1470 // JavaScript enabled. 1471 $this->add_front_page_course_essentials($this->rootnodes['site'], $SITE); 1472 $this->load_course_sections($SITE, $this->rootnodes['site']); 1473 1474 $course = $this->page->course; 1475 $this->load_courses_enrolled(); 1476 1477 // $issite gets set to true if the current pages course is the sites frontpage course 1478 $issite = ($this->page->course->id == $SITE->id); 1479 1480 // Determine if the user is enrolled in any course. 1481 $enrolledinanycourse = enrol_user_sees_own_courses(); 1482 1483 $this->rootnodes['currentcourse']->mainnavonly = true; 1484 if ($enrolledinanycourse) { 1485 $this->rootnodes['mycourses']->isexpandable = true; 1486 $this->rootnodes['mycourses']->showinflatnavigation = true; 1487 if ($CFG->navshowallcourses) { 1488 // When we show all courses we need to show both the my courses and the regular courses branch. 1489 $this->rootnodes['courses']->isexpandable = true; 1490 } 1491 } else { 1492 $this->rootnodes['courses']->isexpandable = true; 1493 } 1494 $this->rootnodes['mycourses']->forceopen = true; 1495 1496 $canviewcourseprofile = true; 1497 1498 // Next load context specific content into the navigation 1499 switch ($this->page->context->contextlevel) { 1500 case CONTEXT_SYSTEM : 1501 // Nothing left to do here I feel. 1502 break; 1503 case CONTEXT_COURSECAT : 1504 // This is essential, we must load categories. 1505 $this->load_all_categories($this->page->context->instanceid, true); 1506 break; 1507 case CONTEXT_BLOCK : 1508 case CONTEXT_COURSE : 1509 if ($issite) { 1510 // Nothing left to do here. 1511 break; 1512 } 1513 1514 // Load the course associated with the current page into the navigation. 1515 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 1516 // If the course wasn't added then don't try going any further. 1517 if (!$coursenode) { 1518 $canviewcourseprofile = false; 1519 break; 1520 } 1521 1522 // If the user is not enrolled then we only want to show the 1523 // course node and not populate it. 1524 1525 // Not enrolled, can't view, and hasn't switched roles 1526 if (!can_access_course($course, null, '', true)) { 1527 if ($coursenode->isexpandable === true) { 1528 // Obviously the situation has changed, update the cache and adjust the node. 1529 // This occurs if the user access to a course has been revoked (one way or another) after 1530 // initially logging in for this session. 1531 $this->get_expand_course_cache()->set($course->id, 1); 1532 $coursenode->isexpandable = true; 1533 $coursenode->nodetype = self::NODETYPE_BRANCH; 1534 } 1535 // Very ugly hack - do not force "parents" to enrol into course their child is enrolled in, 1536 // this hack has been propagated from user/view.php to display the navigation node. (MDL-25805) 1537 if (!$this->current_user_is_parent_role()) { 1538 $coursenode->make_active(); 1539 $canviewcourseprofile = false; 1540 break; 1541 } 1542 } else if ($coursenode->isexpandable === false) { 1543 // Obviously the situation has changed, update the cache and adjust the node. 1544 // This occurs if the user has been granted access to a course (one way or another) after initially 1545 // logging in for this session. 1546 $this->get_expand_course_cache()->set($course->id, 1); 1547 $coursenode->isexpandable = true; 1548 $coursenode->nodetype = self::NODETYPE_BRANCH; 1549 } 1550 1551 // Add the essentials such as reports etc... 1552 $this->add_course_essentials($coursenode, $course); 1553 // Extend course navigation with it's sections/activities 1554 $this->load_course_sections($course, $coursenode); 1555 if (!$coursenode->contains_active_node() && !$coursenode->search_for_active_node()) { 1556 $coursenode->make_active(); 1557 } 1558 1559 break; 1560 case CONTEXT_MODULE : 1561 if ($issite) { 1562 // If this is the site course then most information will have 1563 // already been loaded. 1564 // However we need to check if there is more content that can 1565 // yet be loaded for the specific module instance. 1566 $activitynode = $this->rootnodes['site']->find($this->page->cm->id, navigation_node::TYPE_ACTIVITY); 1567 if ($activitynode) { 1568 $this->load_activity($this->page->cm, $this->page->course, $activitynode); 1569 } 1570 break; 1571 } 1572 1573 $course = $this->page->course; 1574 $cm = $this->page->cm; 1575 1576 // Load the course associated with the page into the navigation 1577 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 1578 1579 // If the course wasn't added then don't try going any further. 1580 if (!$coursenode) { 1581 $canviewcourseprofile = false; 1582 break; 1583 } 1584 1585 // If the user is not enrolled then we only want to show the 1586 // course node and not populate it. 1587 if (!can_access_course($course, null, '', true)) { 1588 $coursenode->make_active(); 1589 $canviewcourseprofile = false; 1590 break; 1591 } 1592 1593 $this->add_course_essentials($coursenode, $course); 1594 1595 // Load the course sections into the page 1596 $this->load_course_sections($course, $coursenode, null, $cm); 1597 $activity = $coursenode->find($cm->id, navigation_node::TYPE_ACTIVITY); 1598 if (!empty($activity)) { 1599 // Finally load the cm specific navigaton information 1600 $this->load_activity($cm, $course, $activity); 1601 // Check if we have an active ndoe 1602 if (!$activity->contains_active_node() && !$activity->search_for_active_node()) { 1603 // And make the activity node active. 1604 $activity->make_active(); 1605 } 1606 } 1607 break; 1608 case CONTEXT_USER : 1609 if ($issite) { 1610 // The users profile information etc is already loaded 1611 // for the front page. 1612 break; 1613 } 1614 $course = $this->page->course; 1615 // Load the course associated with the user into the navigation 1616 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 1617 1618 // If the course wasn't added then don't try going any further. 1619 if (!$coursenode) { 1620 $canviewcourseprofile = false; 1621 break; 1622 } 1623 1624 // If the user is not enrolled then we only want to show the 1625 // course node and not populate it. 1626 if (!can_access_course($course, null, '', true)) { 1627 $coursenode->make_active(); 1628 $canviewcourseprofile = false; 1629 break; 1630 } 1631 $this->add_course_essentials($coursenode, $course); 1632 $this->load_course_sections($course, $coursenode); 1633 break; 1634 } 1635 1636 // Load for the current user 1637 $this->load_for_user(); 1638 if ($this->page->context->contextlevel >= CONTEXT_COURSE && $this->page->context->instanceid != $SITE->id && $canviewcourseprofile) { 1639 $this->load_for_user(null, true); 1640 } 1641 // Load each extending user into the navigation. 1642 foreach ($this->extendforuser as $user) { 1643 if ($user->id != $USER->id) { 1644 $this->load_for_user($user); 1645 } 1646 } 1647 1648 // Give the local plugins a chance to include some navigation if they want. 1649 $this->load_local_plugin_navigation(); 1650 1651 // Remove any empty root nodes 1652 foreach ($this->rootnodes as $node) { 1653 // Dont remove the home node 1654 /** @var navigation_node $node */ 1655 if (!in_array($node->key, ['home', 'mycourses', 'myhome']) && !$node->has_children() && !$node->isactive) { 1656 $node->remove(); 1657 } 1658 } 1659 1660 if (!$this->contains_active_node()) { 1661 $this->search_for_active_node(); 1662 } 1663 1664 // If the user is not logged in modify the navigation structure. 1665 if (!isloggedin()) { 1666 $activities = clone($this->rootnodes['site']->children); 1667 $this->rootnodes['site']->remove(); 1668 $children = clone($this->children); 1669 $this->children = new navigation_node_collection(); 1670 foreach ($activities as $child) { 1671 $this->children->add($child); 1672 } 1673 foreach ($children as $child) { 1674 $this->children->add($child); 1675 } 1676 } 1677 return true; 1678 } 1679 1680 /** 1681 * This function gives local plugins an opportunity to modify navigation. 1682 */ 1683 protected function load_local_plugin_navigation() { 1684 foreach (get_plugin_list_with_function('local', 'extend_navigation') as $function) { 1685 $function($this); 1686 } 1687 } 1688 1689 /** 1690 * Returns true if the current user is a parent of the user being currently viewed. 1691 * 1692 * If the current user is not viewing another user, or if the current user does not hold any parent roles over the 1693 * other user being viewed this function returns false. 1694 * In order to set the user for whom we are checking against you must call {@link set_userid_for_parent_checks()} 1695 * 1696 * @since Moodle 2.4 1697 * @return bool 1698 */ 1699 protected function current_user_is_parent_role() { 1700 global $USER, $DB; 1701 if ($this->useridtouseforparentchecks && $this->useridtouseforparentchecks != $USER->id) { 1702 $usercontext = context_user::instance($this->useridtouseforparentchecks, MUST_EXIST); 1703 if (!has_capability('moodle/user:viewdetails', $usercontext)) { 1704 return false; 1705 } 1706 if ($DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id))) { 1707 return true; 1708 } 1709 } 1710 return false; 1711 } 1712 1713 /** 1714 * Returns true if courses should be shown within categories on the navigation. 1715 * 1716 * @param bool $ismycourse Set to true if you are calculating this for a course. 1717 * @return bool 1718 */ 1719 protected function show_categories($ismycourse = false) { 1720 global $CFG, $DB; 1721 if ($ismycourse) { 1722 return $this->show_my_categories(); 1723 } 1724 if ($this->showcategories === null) { 1725 $show = false; 1726 if ($this->page->context->contextlevel == CONTEXT_COURSECAT) { 1727 $show = true; 1728 } else if (!empty($CFG->navshowcategories) && $DB->count_records('course_categories') > 1) { 1729 $show = true; 1730 } 1731 $this->showcategories = $show; 1732 } 1733 return $this->showcategories; 1734 } 1735 1736 /** 1737 * Returns true if we should show categories in the My Courses branch. 1738 * @return bool 1739 */ 1740 protected function show_my_categories() { 1741 global $CFG; 1742 if ($this->showmycategories === null) { 1743 $this->showmycategories = !empty($CFG->navshowmycoursecategories) && !core_course_category::is_simple_site(); 1744 } 1745 return $this->showmycategories; 1746 } 1747 1748 /** 1749 * Loads the courses in Moodle into the navigation. 1750 * 1751 * @global moodle_database $DB 1752 * @param string|array $categoryids An array containing categories to load courses 1753 * for, OR null to load courses for all categories. 1754 * @return array An array of navigation_nodes one for each course 1755 */ 1756 protected function load_all_courses($categoryids = null) { 1757 global $CFG, $DB, $SITE; 1758 1759 // Work out the limit of courses. 1760 $limit = 20; 1761 if (!empty($CFG->navcourselimit)) { 1762 $limit = $CFG->navcourselimit; 1763 } 1764 1765 $toload = (empty($CFG->navshowallcourses))?self::LOAD_ROOT_CATEGORIES:self::LOAD_ALL_CATEGORIES; 1766 1767 // If we are going to show all courses AND we are showing categories then 1768 // to save us repeated DB calls load all of the categories now 1769 if ($this->show_categories()) { 1770 $this->load_all_categories($toload); 1771 } 1772 1773 // Will be the return of our efforts 1774 $coursenodes = array(); 1775 1776 // Check if we need to show categories. 1777 if ($this->show_categories()) { 1778 // Hmmm we need to show categories... this is going to be painful. 1779 // We now need to fetch up to $limit courses for each category to 1780 // be displayed. 1781 if ($categoryids !== null) { 1782 if (!is_array($categoryids)) { 1783 $categoryids = array($categoryids); 1784 } 1785 list($categorywhere, $categoryparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cc'); 1786 $categorywhere = 'WHERE cc.id '.$categorywhere; 1787 } else if ($toload == self::LOAD_ROOT_CATEGORIES) { 1788 $categorywhere = 'WHERE cc.depth = 1 OR cc.depth = 2'; 1789 $categoryparams = array(); 1790 } else { 1791 $categorywhere = ''; 1792 $categoryparams = array(); 1793 } 1794 1795 // First up we are going to get the categories that we are going to 1796 // need so that we can determine how best to load the courses from them. 1797 $sql = "SELECT cc.id, COUNT(c.id) AS coursecount 1798 FROM {course_categories} cc 1799 LEFT JOIN {course} c ON c.category = cc.id 1800 {$categorywhere} 1801 GROUP BY cc.id"; 1802 $categories = $DB->get_recordset_sql($sql, $categoryparams); 1803 $fullfetch = array(); 1804 $partfetch = array(); 1805 foreach ($categories as $category) { 1806 if (!$this->can_add_more_courses_to_category($category->id)) { 1807 continue; 1808 } 1809 if ($category->coursecount > $limit * 5) { 1810 $partfetch[] = $category->id; 1811 } else if ($category->coursecount > 0) { 1812 $fullfetch[] = $category->id; 1813 } 1814 } 1815 $categories->close(); 1816 1817 if (count($fullfetch)) { 1818 // First up fetch all of the courses in categories where we know that we are going to 1819 // need the majority of courses. 1820 list($categoryids, $categoryparams) = $DB->get_in_or_equal($fullfetch, SQL_PARAMS_NAMED, 'lcategory'); 1821 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); 1822 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; 1823 $categoryparams['contextlevel'] = CONTEXT_COURSE; 1824 $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect 1825 FROM {course} c 1826 $ccjoin 1827 WHERE c.category {$categoryids} 1828 ORDER BY c.sortorder ASC"; 1829 $coursesrs = $DB->get_recordset_sql($sql, $categoryparams); 1830 foreach ($coursesrs as $course) { 1831 if ($course->id == $SITE->id) { 1832 // This should not be necessary, frontpage is not in any category. 1833 continue; 1834 } 1835 if (array_key_exists($course->id, $this->addedcourses)) { 1836 // It is probably better to not include the already loaded courses 1837 // directly in SQL because inequalities may confuse query optimisers 1838 // and may interfere with query caching. 1839 continue; 1840 } 1841 if (!$this->can_add_more_courses_to_category($course->category)) { 1842 continue; 1843 } 1844 context_helper::preload_from_record($course); 1845 if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { 1846 continue; 1847 } 1848 $coursenodes[$course->id] = $this->add_course($course); 1849 } 1850 $coursesrs->close(); 1851 } 1852 1853 if (count($partfetch)) { 1854 // Next we will work our way through the categories where we will likely only need a small 1855 // proportion of the courses. 1856 foreach ($partfetch as $categoryid) { 1857 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); 1858 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; 1859 $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect 1860 FROM {course} c 1861 $ccjoin 1862 WHERE c.category = :categoryid 1863 ORDER BY c.sortorder ASC"; 1864 $courseparams = array('categoryid' => $categoryid, 'contextlevel' => CONTEXT_COURSE); 1865 $coursesrs = $DB->get_recordset_sql($sql, $courseparams, 0, $limit * 5); 1866 foreach ($coursesrs as $course) { 1867 if ($course->id == $SITE->id) { 1868 // This should not be necessary, frontpage is not in any category. 1869 continue; 1870 } 1871 if (array_key_exists($course->id, $this->addedcourses)) { 1872 // It is probably better to not include the already loaded courses 1873 // directly in SQL because inequalities may confuse query optimisers 1874 // and may interfere with query caching. 1875 // This also helps to respect expected $limit on repeated executions. 1876 continue; 1877 } 1878 if (!$this->can_add_more_courses_to_category($course->category)) { 1879 break; 1880 } 1881 context_helper::preload_from_record($course); 1882 if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { 1883 continue; 1884 } 1885 $coursenodes[$course->id] = $this->add_course($course); 1886 } 1887 $coursesrs->close(); 1888 } 1889 } 1890 } else { 1891 // Prepare the SQL to load the courses and their contexts 1892 list($courseids, $courseparams) = $DB->get_in_or_equal(array_keys($this->addedcourses), SQL_PARAMS_NAMED, 'lc', false); 1893 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); 1894 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; 1895 $courseparams['contextlevel'] = CONTEXT_COURSE; 1896 $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect 1897 FROM {course} c 1898 $ccjoin 1899 WHERE c.id {$courseids} 1900 ORDER BY c.sortorder ASC"; 1901 $coursesrs = $DB->get_recordset_sql($sql, $courseparams); 1902 foreach ($coursesrs as $course) { 1903 if ($course->id == $SITE->id) { 1904 // frotpage is not wanted here 1905 continue; 1906 } 1907 if ($this->page->course && ($this->page->course->id == $course->id)) { 1908 // Don't include the currentcourse in this nodelist - it's displayed in the Current course node 1909 continue; 1910 } 1911 context_helper::preload_from_record($course); 1912 if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { 1913 continue; 1914 } 1915 $coursenodes[$course->id] = $this->add_course($course); 1916 if (count($coursenodes) >= $limit) { 1917 break; 1918 } 1919 } 1920 $coursesrs->close(); 1921 } 1922 1923 return $coursenodes; 1924 } 1925 1926 /** 1927 * Returns true if more courses can be added to the provided category. 1928 * 1929 * @param int|navigation_node|stdClass $category 1930 * @return bool 1931 */ 1932 protected function can_add_more_courses_to_category($category) { 1933 global $CFG; 1934 $limit = 20; 1935 if (!empty($CFG->navcourselimit)) { 1936 $limit = (int)$CFG->navcourselimit; 1937 } 1938 if (is_numeric($category)) { 1939 if (!array_key_exists($category, $this->addedcategories)) { 1940 return true; 1941 } 1942 $coursecount = count($this->addedcategories[$category]->children->type(self::TYPE_COURSE)); 1943 } else if ($category instanceof navigation_node) { 1944 if (($category->type != self::TYPE_CATEGORY) || ($category->type != self::TYPE_MY_CATEGORY)) { 1945 return false; 1946 } 1947 $coursecount = count($category->children->type(self::TYPE_COURSE)); 1948 } else if (is_object($category) && property_exists($category,'id')) { 1949 $coursecount = count($this->addedcategories[$category->id]->children->type(self::TYPE_COURSE)); 1950 } 1951 return ($coursecount <= $limit); 1952 } 1953 1954 /** 1955 * Loads all categories (top level or if an id is specified for that category) 1956 * 1957 * @param int $categoryid The category id to load or null/0 to load all base level categories 1958 * @param bool $showbasecategories If set to true all base level categories will be loaded as well 1959 * as the requested category and any parent categories. 1960 * @return navigation_node|void returns a navigation node if a category has been loaded. 1961 */ 1962 protected function load_all_categories($categoryid = self::LOAD_ROOT_CATEGORIES, $showbasecategories = false) { 1963 global $CFG, $DB; 1964 1965 // Check if this category has already been loaded 1966 if ($this->allcategoriesloaded || ($categoryid < 1 && $this->is_category_fully_loaded($categoryid))) { 1967 return true; 1968 } 1969 1970 $catcontextsql = context_helper::get_preload_record_columns_sql('ctx'); 1971 $sqlselect = "SELECT cc.*, $catcontextsql 1972 FROM {course_categories} cc 1973 JOIN {context} ctx ON cc.id = ctx.instanceid"; 1974 $sqlwhere = "WHERE ctx.contextlevel = ".CONTEXT_COURSECAT; 1975 $sqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC, cc.id ASC"; 1976 $params = array(); 1977 1978 $categoriestoload = array(); 1979 if ($categoryid == self::LOAD_ALL_CATEGORIES) { 1980 // We are going to load all categories regardless... prepare to fire 1981 // on the database server! 1982 } else if ($categoryid == self::LOAD_ROOT_CATEGORIES) { // can be 0 1983 // We are going to load all of the first level categories (categories without parents) 1984 $sqlwhere .= " AND cc.parent = 0"; 1985 } else if (array_key_exists($categoryid, $this->addedcategories)) { 1986 // The category itself has been loaded already so we just need to ensure its subcategories 1987 // have been loaded 1988 $addedcategories = $this->addedcategories; 1989 unset($addedcategories[$categoryid]); 1990 if (count($addedcategories) > 0) { 1991 list($sql, $params) = $DB->get_in_or_equal(array_keys($addedcategories), SQL_PARAMS_NAMED, 'parent', false); 1992 if ($showbasecategories) { 1993 // We need to include categories with parent = 0 as well 1994 $sqlwhere .= " AND (cc.parent = :categoryid OR cc.parent = 0) AND cc.parent {$sql}"; 1995 } else { 1996 // All we need is categories that match the parent 1997 $sqlwhere .= " AND cc.parent = :categoryid AND cc.parent {$sql}"; 1998 } 1999 } 2000 $params['categoryid'] = $categoryid; 2001 } else { 2002 // This category hasn't been loaded yet so we need to fetch it, work out its category path 2003 // and load this category plus all its parents and subcategories 2004 $category = $DB->get_record('course_categories', array('id' => $categoryid), 'path', MUST_EXIST); 2005 $categoriestoload = explode('/', trim($category->path, '/')); 2006 list($select, $params) = $DB->get_in_or_equal($categoriestoload); 2007 // We are going to use select twice so double the params 2008 $params = array_merge($params, $params); 2009 $basecategorysql = ($showbasecategories)?' OR cc.depth = 1':''; 2010 $sqlwhere .= " AND (cc.id {$select} OR cc.parent {$select}{$basecategorysql})"; 2011 } 2012 2013 $categoriesrs = $DB->get_recordset_sql("$sqlselect $sqlwhere $sqlorder", $params); 2014 $categories = array(); 2015 foreach ($categoriesrs as $category) { 2016 // Preload the context.. we'll need it when adding the category in order 2017 // to format the category name. 2018 context_helper::preload_from_record($category); 2019 if (array_key_exists($category->id, $this->addedcategories)) { 2020 // Do nothing, its already been added. 2021 } else if ($category->parent == '0') { 2022 // This is a root category lets add it immediately 2023 $this->add_category($category, $this->rootnodes['courses']); 2024 } else if (array_key_exists($category->parent, $this->addedcategories)) { 2025 // This categories parent has already been added we can add this immediately 2026 $this->add_category($category, $this->addedcategories[$category->parent]); 2027 } else { 2028 $categories[] = $category; 2029 } 2030 } 2031 $categoriesrs->close(); 2032 2033 // Now we have an array of categories we need to add them to the navigation. 2034 while (!empty($categories)) { 2035 $category = reset($categories); 2036 if (array_key_exists($category->id, $this->addedcategories)) { 2037 // Do nothing 2038 } else if ($category->parent == '0') { 2039 $this->add_category($category, $this->rootnodes['courses']); 2040 } else if (array_key_exists($category->parent, $this->addedcategories)) { 2041 $this->add_category($category, $this->addedcategories[$category->parent]); 2042 } else { 2043 // This category isn't in the navigation and niether is it's parent (yet). 2044 // We need to go through the category path and add all of its components in order. 2045 $path = explode('/', trim($category->path, '/')); 2046 foreach ($path as $catid) { 2047 if (!array_key_exists($catid, $this->addedcategories)) { 2048 // This category isn't in the navigation yet so add it. 2049 $subcategory = $categories[$catid]; 2050 if ($subcategory->parent == '0') { 2051 // Yay we have a root category - this likely means we will now be able 2052 // to add categories without problems. 2053 $this->add_category($subcategory, $this->rootnodes['courses']); 2054 } else if (array_key_exists($subcategory->parent, $this->addedcategories)) { 2055 // The parent is in the category (as we'd expect) so add it now. 2056 $this->add_category($subcategory, $this->addedcategories[$subcategory->parent]); 2057 // Remove the category from the categories array. 2058 unset($categories[$catid]); 2059 } else { 2060 // We should never ever arrive here - if we have then there is a bigger 2061 // problem at hand. 2062 throw new coding_exception('Category path order is incorrect and/or there are missing categories'); 2063 } 2064 } 2065 } 2066 } 2067 // Remove the category from the categories array now that we know it has been added. 2068 unset($categories[$category->id]); 2069 } 2070 if ($categoryid === self::LOAD_ALL_CATEGORIES) { 2071 $this->allcategoriesloaded = true; 2072 } 2073 // Check if there are any categories to load. 2074 if (count($categoriestoload) > 0) { 2075 $readytoloadcourses = array(); 2076 foreach ($categoriestoload as $category) { 2077 if ($this->can_add_more_courses_to_category($category)) { 2078 $readytoloadcourses[] = $category; 2079 } 2080 } 2081 if (count($readytoloadcourses)) { 2082 $this->load_all_courses($readytoloadcourses); 2083 } 2084 } 2085 2086 // Look for all categories which have been loaded 2087 if (!empty($this->addedcategories)) { 2088 $categoryids = array(); 2089 foreach ($this->addedcategories as $category) { 2090 if ($this->can_add_more_courses_to_category($category)) { 2091 $categoryids[] = $category->key; 2092 } 2093 } 2094 if ($categoryids) { 2095 list($categoriessql, $params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED); 2096 $params['limit'] = (!empty($CFG->navcourselimit))?$CFG->navcourselimit:20; 2097 $sql = "SELECT cc.id, COUNT(c.id) AS coursecount 2098 FROM {course_categories} cc 2099 JOIN {course} c ON c.category = cc.id 2100 WHERE cc.id {$categoriessql} 2101 GROUP BY cc.id 2102 HAVING COUNT(c.id) > :limit"; 2103 $excessivecategories = $DB->get_records_sql($sql, $params); 2104 foreach ($categories as &$category) { 2105 if (array_key_exists($category->key, $excessivecategories) && !$this->can_add_more_courses_to_category($category)) { 2106 $url = new moodle_url('/course/index.php', array('categoryid' => $category->key)); 2107 $category->add(get_string('viewallcourses'), $url, self::TYPE_SETTING); 2108 } 2109 } 2110 } 2111 } 2112 } 2113 2114 /** 2115 * Adds a structured category to the navigation in the correct order/place 2116 * 2117 * @param stdClass $category category to be added in navigation. 2118 * @param navigation_node $parent parent navigation node 2119 * @param int $nodetype type of node, if category is under MyHome then it's TYPE_MY_CATEGORY 2120 * @return void. 2121 */ 2122 protected function add_category(stdClass $category, navigation_node $parent, $nodetype = self::TYPE_CATEGORY) { 2123 global $CFG; 2124 if (array_key_exists($category->id, $this->addedcategories)) { 2125 return; 2126 } 2127 $canview = core_course_category::can_view_category($category); 2128 $url = $canview ? new moodle_url('/course/index.php', array('categoryid' => $category->id)) : null; 2129 $context = \context_helper::get_navigation_filter_context(context_coursecat::instance($category->id)); 2130 $categoryname = $canview ? format_string($category->name, true, ['context' => $context]) : 2131 get_string('categoryhidden'); 2132 $categorynode = $parent->add($categoryname, $url, $nodetype, $categoryname, $category->id); 2133 if (!$canview) { 2134 // User does not have required capabilities to view category. 2135 $categorynode->display = false; 2136 } else if (!$category->visible) { 2137 // Category is hidden but user has capability to view hidden categories. 2138 $categorynode->hidden = true; 2139 } 2140 $this->addedcategories[$category->id] = $categorynode; 2141 } 2142 2143 /** 2144 * Loads the given course into the navigation 2145 * 2146 * @param stdClass $course 2147 * @return navigation_node 2148 */ 2149 protected function load_course(stdClass $course) { 2150 global $SITE; 2151 if ($course->id == $SITE->id) { 2152 // This is always loaded during initialisation 2153 return $this->rootnodes['site']; 2154 } else if (array_key_exists($course->id, $this->addedcourses)) { 2155 // The course has already been loaded so return a reference 2156 return $this->addedcourses[$course->id]; 2157 } else { 2158 // Add the course 2159 return $this->add_course($course); 2160 } 2161 } 2162 2163 /** 2164 * Loads all of the courses section into the navigation. 2165 * 2166 * This function calls method from current course format, see 2167 * core_courseformat\base::extend_course_navigation() 2168 * If course module ($cm) is specified but course format failed to create the node, 2169 * the activity node is created anyway. 2170 * 2171 * By default course formats call the method global_navigation::load_generic_course_sections() 2172 * 2173 * @param stdClass $course Database record for the course 2174 * @param navigation_node $coursenode The course node within the navigation 2175 * @param null|int $sectionnum If specified load the contents of section with this relative number 2176 * @param null|cm_info $cm If specified make sure that activity node is created (either 2177 * in containg section or by calling load_stealth_activity() ) 2178 */ 2179 protected function load_course_sections(stdClass $course, navigation_node $coursenode, $sectionnum = null, $cm = null) { 2180 global $CFG, $SITE; 2181 require_once($CFG->dirroot.'/course/lib.php'); 2182 if (isset($cm->sectionnum)) { 2183 $sectionnum = $cm->sectionnum; 2184 } 2185 if ($sectionnum !== null) { 2186 $this->includesectionnum = $sectionnum; 2187 } 2188 course_get_format($course)->extend_course_navigation($this, $coursenode, $sectionnum, $cm); 2189 if (isset($cm->id)) { 2190 $activity = $coursenode->find($cm->id, self::TYPE_ACTIVITY); 2191 if (empty($activity)) { 2192 $activity = $this->load_stealth_activity($coursenode, get_fast_modinfo($course)); 2193 } 2194 } 2195 } 2196 2197 /** 2198 * Generates an array of sections and an array of activities for the given course. 2199 * 2200 * This method uses the cache to improve performance and avoid the get_fast_modinfo call 2201 * 2202 * @param stdClass $course 2203 * @return array Array($sections, $activities) 2204 */ 2205 protected function generate_sections_and_activities(stdClass $course) { 2206 global $CFG; 2207 require_once($CFG->dirroot.'/course/lib.php'); 2208 2209 $modinfo = get_fast_modinfo($course); 2210 $sections = $modinfo->get_section_info_all(); 2211 2212 // For course formats using 'numsections' trim the sections list 2213 $courseformatoptions = course_get_format($course)->get_format_options(); 2214 if (isset($courseformatoptions['numsections'])) { 2215 $sections = array_slice($sections, 0, $courseformatoptions['numsections']+1, true); 2216 } 2217 2218 $activities = array(); 2219 2220 foreach ($sections as $key => $section) { 2221 // Clone and unset summary to prevent $SESSION bloat (MDL-31802). 2222 $sections[$key] = clone($section); 2223 unset($sections[$key]->summary); 2224 $sections[$key]->hasactivites = false; 2225 if (!array_key_exists($section->section, $modinfo->sections)) { 2226 continue; 2227 } 2228 foreach ($modinfo->sections[$section->section] as $cmid) { 2229 $cm = $modinfo->cms[$cmid]; 2230 $activity = new stdClass; 2231 $activity->id = $cm->id; 2232 $activity->course = $course->id; 2233 $activity->section = $section->section; 2234 $activity->name = $cm->name; 2235 $activity->icon = $cm->icon; 2236 $activity->iconcomponent = $cm->iconcomponent; 2237 $activity->hidden = (!$cm->visible); 2238 $activity->modname = $cm->modname; 2239 $activity->nodetype = navigation_node::NODETYPE_LEAF; 2240 $activity->onclick = $cm->onclick; 2241 $url = $cm->url; 2242 if (!$url) { 2243 $activity->url = null; 2244 $activity->display = false; 2245 } else { 2246 $activity->url = $url->out(); 2247 $activity->display = $cm->is_visible_on_course_page() ? true : false; 2248 if (self::module_extends_navigation($cm->modname)) { 2249 $activity->nodetype = navigation_node::NODETYPE_BRANCH; 2250 } 2251 } 2252 $activities[$cmid] = $activity; 2253 if ($activity->display) { 2254 $sections[$key]->hasactivites = true; 2255 } 2256 } 2257 } 2258 2259 return array($sections, $activities); 2260 } 2261 2262 /** 2263 * Generically loads the course sections into the course's navigation. 2264 * 2265 * @param stdClass $course 2266 * @param navigation_node $coursenode 2267 * @return array An array of course section nodes 2268 */ 2269 public function load_generic_course_sections(stdClass $course, navigation_node $coursenode) { 2270 global $CFG, $DB, $USER, $SITE; 2271 require_once($CFG->dirroot.'/course/lib.php'); 2272 2273 list($sections, $activities) = $this->generate_sections_and_activities($course); 2274 2275 $navigationsections = array(); 2276 foreach ($sections as $sectionid => $section) { 2277 $section = clone($section); 2278 if ($course->id == $SITE->id) { 2279 $this->load_section_activities($coursenode, $section->section, $activities); 2280 } else { 2281 if (!$section->uservisible || (!$this->showemptysections && 2282 !$section->hasactivites && $this->includesectionnum !== $section->section)) { 2283 continue; 2284 } 2285 2286 $sectionname = get_section_name($course, $section); 2287 $url = course_get_url($course, $section->section, array('navigation' => true)); 2288 2289 $sectionnode = $coursenode->add($sectionname, $url, navigation_node::TYPE_SECTION, 2290 null, $section->id, new pix_icon('i/section', '')); 2291 $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; 2292 $sectionnode->hidden = (!$section->visible || !$section->available); 2293 if ($this->includesectionnum !== false && $this->includesectionnum == $section->section) { 2294 $this->load_section_activities($sectionnode, $section->section, $activities); 2295 } 2296 $navigationsections[$sectionid] = $section; 2297 } 2298 } 2299 return $navigationsections; 2300 } 2301 2302 /** 2303 * Loads all of the activities for a section into the navigation structure. 2304 * 2305 * @param navigation_node $sectionnode 2306 * @param int $sectionnumber 2307 * @param array $activities An array of activites as returned by {@link global_navigation::generate_sections_and_activities()} 2308 * @param stdClass $course The course object the section and activities relate to. 2309 * @return array Array of activity nodes 2310 */ 2311 protected function load_section_activities(navigation_node $sectionnode, $sectionnumber, array $activities, $course = null) { 2312 global $CFG, $SITE; 2313 // A static counter for JS function naming 2314 static $legacyonclickcounter = 0; 2315 2316 $activitynodes = array(); 2317 if (empty($activities)) { 2318 return $activitynodes; 2319 } 2320 2321 if (!is_object($course)) { 2322 $activity = reset($activities); 2323 $courseid = $activity->course; 2324 } else { 2325 $courseid = $course->id; 2326 } 2327 $showactivities = ($courseid != $SITE->id || !empty($CFG->navshowfrontpagemods)); 2328 2329 foreach ($activities as $activity) { 2330 if ($activity->section != $sectionnumber) { 2331 continue; 2332 } 2333 if ($activity->icon) { 2334 $icon = new pix_icon($activity->icon, get_string('modulename', $activity->modname), $activity->iconcomponent); 2335 } else { 2336 $icon = new pix_icon('monologo', get_string('modulename', $activity->modname), $activity->modname); 2337 } 2338 2339 // Prepare the default name and url for the node 2340 $displaycontext = \context_helper::get_navigation_filter_context(context_module::instance($activity->id)); 2341 $activityname = format_string($activity->name, true, ['context' => $displaycontext]); 2342 $action = new moodle_url($activity->url); 2343 2344 // Check if the onclick property is set (puke!) 2345 if (!empty($activity->onclick)) { 2346 // Increment the counter so that we have a unique number. 2347 $legacyonclickcounter++; 2348 // Generate the function name we will use 2349 $functionname = 'legacy_activity_onclick_handler_'.$legacyonclickcounter; 2350 $propogrationhandler = ''; 2351 // Check if we need to cancel propogation. Remember inline onclick 2352 // events would return false if they wanted to prevent propogation and the 2353 // default action. 2354 if (strpos($activity->onclick, 'return false')) { 2355 $propogrationhandler = 'e.halt();'; 2356 } 2357 // Decode the onclick - it has already been encoded for display (puke) 2358 $onclick = htmlspecialchars_decode($activity->onclick, ENT_QUOTES); 2359 // Build the JS function the click event will call 2360 $jscode = "function {$functionname}(e) { $propogrationhandler $onclick }"; 2361 $this->page->requires->js_amd_inline($jscode); 2362 // Override the default url with the new action link 2363 $action = new action_link($action, $activityname, new component_action('click', $functionname)); 2364 } 2365 2366 $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon); 2367 $activitynode->title(get_string('modulename', $activity->modname)); 2368 $activitynode->hidden = $activity->hidden; 2369 $activitynode->display = $showactivities && $activity->display; 2370 $activitynode->nodetype = $activity->nodetype; 2371 $activitynodes[$activity->id] = $activitynode; 2372 } 2373 2374 return $activitynodes; 2375 } 2376 /** 2377 * Loads a stealth module from unavailable section 2378 * @param navigation_node $coursenode 2379 * @param stdClass|course_modinfo $modinfo 2380 * @return navigation_node or null if not accessible 2381 */ 2382 protected function load_stealth_activity(navigation_node $coursenode, $modinfo) { 2383 if (empty($modinfo->cms[$this->page->cm->id])) { 2384 return null; 2385 } 2386 $cm = $modinfo->cms[$this->page->cm->id]; 2387 if ($cm->icon) { 2388 $icon = new pix_icon($cm->icon, get_string('modulename', $cm->modname), $cm->iconcomponent); 2389 } else { 2390 $icon = new pix_icon('monologo', get_string('modulename', $cm->modname), $cm->modname); 2391 } 2392 $url = $cm->url; 2393 $activitynode = $coursenode->add(format_string($cm->name), $url, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon); 2394 $activitynode->title(get_string('modulename', $cm->modname)); 2395 $activitynode->hidden = (!$cm->visible); 2396 if (!$cm->is_visible_on_course_page()) { 2397 // Do not show any error here, let the page handle exception that activity is not visible for the current user. 2398 // Also there may be no exception at all in case when teacher is logged in as student. 2399 $activitynode->display = false; 2400 } else if (!$url) { 2401 // Don't show activities that don't have links! 2402 $activitynode->display = false; 2403 } else if (self::module_extends_navigation($cm->modname)) { 2404 $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; 2405 } 2406 return $activitynode; 2407 } 2408 /** 2409 * Loads the navigation structure for the given activity into the activities node. 2410 * 2411 * This method utilises a callback within the modules lib.php file to load the 2412 * content specific to activity given. 2413 * 2414 * The callback is a method: {modulename}_extend_navigation() 2415 * Examples: 2416 * * {@link forum_extend_navigation()} 2417 * * {@link workshop_extend_navigation()} 2418 * 2419 * @param cm_info|stdClass $cm 2420 * @param stdClass $course 2421 * @param navigation_node $activity 2422 * @return bool 2423 */ 2424 protected function load_activity($cm, stdClass $course, navigation_node $activity) { 2425 global $CFG, $DB; 2426 2427 // make sure we have a $cm from get_fast_modinfo as this contains activity access details 2428 if (!($cm instanceof cm_info)) { 2429 $modinfo = get_fast_modinfo($course); 2430 $cm = $modinfo->get_cm($cm->id); 2431 } 2432 $activity->nodetype = navigation_node::NODETYPE_LEAF; 2433 $activity->make_active(); 2434 $file = $CFG->dirroot.'/mod/'.$cm->modname.'/lib.php'; 2435 $function = $cm->modname.'_extend_navigation'; 2436 2437 if (file_exists($file)) { 2438 require_once($file); 2439 if (function_exists($function)) { 2440 $activtyrecord = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); 2441 $function($activity, $course, $activtyrecord, $cm); 2442 } 2443 } 2444 2445 // Allow the active advanced grading method plugin to append module navigation 2446 $featuresfunc = $cm->modname.'_supports'; 2447 if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING)) { 2448 require_once($CFG->dirroot.'/grade/grading/lib.php'); 2449 $gradingman = get_grading_manager($cm->context, 'mod_'.$cm->modname); 2450 $gradingman->extend_navigation($this, $activity); 2451 } 2452 2453 return $activity->has_children(); 2454 } 2455 /** 2456 * Loads user specific information into the navigation in the appropriate place. 2457 * 2458 * If no user is provided the current user is assumed. 2459 * 2460 * @param stdClass $user 2461 * @param bool $forceforcontext probably force something to be loaded somewhere (ask SamH if not sure what this means) 2462 * @return bool 2463 */ 2464 protected function load_for_user($user=null, $forceforcontext=false) { 2465 global $DB, $CFG, $USER, $SITE; 2466 2467 require_once($CFG->dirroot . '/course/lib.php'); 2468 2469 if ($user === null) { 2470 // We can't require login here but if the user isn't logged in we don't 2471 // want to show anything 2472 if (!isloggedin() || isguestuser()) { 2473 return false; 2474 } 2475 $user = $USER; 2476 } else if (!is_object($user)) { 2477 // If the user is not an object then get them from the database 2478 $select = context_helper::get_preload_record_columns_sql('ctx'); 2479 $sql = "SELECT u.*, $select 2480 FROM {user} u 2481 JOIN {context} ctx ON u.id = ctx.instanceid 2482 WHERE u.id = :userid AND 2483 ctx.contextlevel = :contextlevel"; 2484 $user = $DB->get_record_sql($sql, array('userid' => (int)$user, 'contextlevel' => CONTEXT_USER), MUST_EXIST); 2485 context_helper::preload_from_record($user); 2486 } 2487 2488 $iscurrentuser = ($user->id == $USER->id); 2489 2490 $usercontext = context_user::instance($user->id); 2491 2492 // Get the course set against the page, by default this will be the site 2493 $course = $this->page->course; 2494 $baseargs = array('id'=>$user->id); 2495 if ($course->id != $SITE->id && (!$iscurrentuser || $forceforcontext)) { 2496 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 2497 $baseargs['course'] = $course->id; 2498 $coursecontext = context_course::instance($course->id); 2499 $issitecourse = false; 2500 } else { 2501 // Load all categories and get the context for the system 2502 $coursecontext = context_system::instance(); 2503 $issitecourse = true; 2504 } 2505 2506 // Create a node to add user information under. 2507 $usersnode = null; 2508 if (!$issitecourse) { 2509 // Not the current user so add it to the participants node for the current course. 2510 $usersnode = $coursenode->get('participants', navigation_node::TYPE_CONTAINER); 2511 $userviewurl = new moodle_url('/user/view.php', $baseargs); 2512 } else if ($USER->id != $user->id) { 2513 // This is the site so add a users node to the root branch. 2514 $usersnode = $this->rootnodes['users']; 2515 if (course_can_view_participants($coursecontext)) { 2516 $usersnode->action = new moodle_url('/user/index.php', array('id' => $course->id)); 2517 } 2518 $userviewurl = new moodle_url('/user/profile.php', $baseargs); 2519 } 2520 if (!$usersnode) { 2521 // We should NEVER get here, if the course hasn't been populated 2522 // with a participants node then the navigaiton either wasn't generated 2523 // for it (you are missing a require_login or set_context call) or 2524 // you don't have access.... in the interests of no leaking informatin 2525 // we simply quit... 2526 return false; 2527 } 2528 // Add a branch for the current user. 2529 // Only reveal user details if $user is the current user, or a user to which the current user has access. 2530 $viewprofile = true; 2531 if (!$iscurrentuser) { 2532 require_once($CFG->dirroot . '/user/lib.php'); 2533 if ($this->page->context->contextlevel == CONTEXT_USER && !has_capability('moodle/user:viewdetails', $usercontext) ) { 2534 $viewprofile = false; 2535 } else if ($this->page->context->contextlevel != CONTEXT_USER && !user_can_view_profile($user, $course, $usercontext)) { 2536 $viewprofile = false; 2537 } 2538 if (!$viewprofile) { 2539 $viewprofile = user_can_view_profile($user, null, $usercontext); 2540 } 2541 } 2542 2543 // Now, conditionally add the user node. 2544 if ($viewprofile) { 2545 $canseefullname = has_capability('moodle/site:viewfullnames', $coursecontext); 2546 $usernode = $usersnode->add(fullname($user, $canseefullname), $userviewurl, self::TYPE_USER, null, 'user' . $user->id); 2547 } else { 2548 $usernode = $usersnode->add(get_string('user')); 2549 } 2550 2551 if ($this->page->context->contextlevel == CONTEXT_USER && $user->id == $this->page->context->instanceid) { 2552 $usernode->make_active(); 2553 } 2554 2555 // Add user information to the participants or user node. 2556 if ($issitecourse) { 2557 2558 // If the user is the current user or has permission to view the details of the requested 2559 // user than add a view profile link. 2560 if ($iscurrentuser || has_capability('moodle/user:viewdetails', $coursecontext) || 2561 has_capability('moodle/user:viewdetails', $usercontext)) { 2562 if ($issitecourse || ($iscurrentuser && !$forceforcontext)) { 2563 $usernode->add(get_string('viewprofile'), new moodle_url('/user/profile.php', $baseargs)); 2564 } else { 2565 $usernode->add(get_string('viewprofile'), new moodle_url('/user/view.php', $baseargs)); 2566 } 2567 } 2568 2569 if (!empty($CFG->navadduserpostslinks)) { 2570 // Add nodes for forum posts and discussions if the user can view either or both 2571 // There are no capability checks here as the content of the page is based 2572 // purely on the forums the current user has access too. 2573 $forumtab = $usernode->add(get_string('forumposts', 'forum')); 2574 $forumtab->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', $baseargs)); 2575 $forumtab->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', 2576 array_merge($baseargs, array('mode' => 'discussions')))); 2577 } 2578 2579 // Add blog nodes. 2580 if (!empty($CFG->enableblogs)) { 2581 if (!$this->cache->cached('userblogoptions'.$user->id)) { 2582 require_once($CFG->dirroot.'/blog/lib.php'); 2583 // Get all options for the user. 2584 $options = blog_get_options_for_user($user); 2585 $this->cache->set('userblogoptions'.$user->id, $options); 2586 } else { 2587 $options = $this->cache->{'userblogoptions'.$user->id}; 2588 } 2589 2590 if (count($options) > 0) { 2591 $blogs = $usernode->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER); 2592 foreach ($options as $type => $option) { 2593 if ($type == "rss") { 2594 $blogs->add($option['string'], $option['link'], settings_navigation::TYPE_SETTING, null, null, 2595 new pix_icon('i/rss', '')); 2596 } else { 2597 $blogs->add($option['string'], $option['link']); 2598 } 2599 } 2600 } 2601 } 2602 2603 // Add the messages link. 2604 // It is context based so can appear in the user's profile and in course participants information. 2605 if (!empty($CFG->messaging)) { 2606 $messageargs = array('user1' => $USER->id); 2607 if ($USER->id != $user->id) { 2608 $messageargs['user2'] = $user->id; 2609 } 2610 $url = new moodle_url('/message/index.php', $messageargs); 2611 $usernode->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages'); 2612 } 2613 2614 // Add the "My private files" link. 2615 // This link doesn't have a unique display for course context so only display it under the user's profile. 2616 if ($issitecourse && $iscurrentuser && has_capability('moodle/user:manageownfiles', $usercontext)) { 2617 $url = new moodle_url('/user/files.php'); 2618 $usernode->add(get_string('privatefiles'), $url, self::TYPE_SETTING, null, 'privatefiles'); 2619 } 2620 2621 // Add a node to view the users notes if permitted. 2622 if (!empty($CFG->enablenotes) && 2623 has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $coursecontext)) { 2624 $url = new moodle_url('/notes/index.php', array('user' => $user->id)); 2625 if ($coursecontext->instanceid != SITEID) { 2626 $url->param('course', $coursecontext->instanceid); 2627 } 2628 $usernode->add(get_string('notes', 'notes'), $url); 2629 } 2630 2631 // Show the grades node. 2632 if (($issitecourse && $iscurrentuser) || has_capability('moodle/user:viewdetails', $usercontext)) { 2633 require_once($CFG->dirroot . '/user/lib.php'); 2634 // Set the grades node to link to the "Grades" page. 2635 if ($course->id == SITEID) { 2636 $url = user_mygrades_url($user->id, $course->id); 2637 } else { // Otherwise we are in a course and should redirect to the user grade report (Activity report version). 2638 $url = new moodle_url('/course/user.php', array('mode' => 'grade', 'id' => $course->id, 'user' => $user->id)); 2639 } 2640 if ($USER->id != $user->id) { 2641 $usernode->add(get_string('grades', 'grades'), $url, self::TYPE_SETTING, null, 'usergrades'); 2642 } else { 2643 $usernode->add(get_string('grades', 'grades'), $url); 2644 } 2645 } 2646 2647 // If the user is the current user add the repositories for the current user. 2648 $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); 2649 if (!$iscurrentuser && 2650 $course->id == $SITE->id && 2651 has_capability('moodle/user:viewdetails', $usercontext) && 2652 (!in_array('mycourses', $hiddenfields) || has_capability('moodle/user:viewhiddendetails', $coursecontext))) { 2653 2654 // Add view grade report is permitted. 2655 $reports = core_component::get_plugin_list('gradereport'); 2656 arsort($reports); // User is last, we want to test it first. 2657 2658 $userscourses = enrol_get_users_courses($user->id, false, '*'); 2659 $userscoursesnode = $usernode->add(get_string('courses')); 2660 2661 $count = 0; 2662 foreach ($userscourses as $usercourse) { 2663 if ($count === (int)$CFG->navcourselimit) { 2664 $url = new moodle_url('/user/profile.php', array('id' => $user->id, 'showallcourses' => 1)); 2665 $userscoursesnode->add(get_string('showallcourses'), $url); 2666 break; 2667 } 2668 $count++; 2669 $usercoursecontext = context_course::instance($usercourse->id); 2670 $usercourseshortname = format_string($usercourse->shortname, true, array('context' => $usercoursecontext)); 2671 $usercoursenode = $userscoursesnode->add($usercourseshortname, new moodle_url('/user/view.php', 2672 array('id' => $user->id, 'course' => $usercourse->id)), self::TYPE_CONTAINER); 2673 2674 $gradeavailable = has_capability('moodle/grade:view', $usercoursecontext); 2675 if (!$gradeavailable && !empty($usercourse->showgrades) && is_array($reports) && !empty($reports)) { 2676 foreach ($reports as $plugin => $plugindir) { 2677 if (has_capability('gradereport/'.$plugin.':view', $usercoursecontext)) { 2678 // Stop when the first visible plugin is found. 2679 $gradeavailable = true; 2680 break; 2681 } 2682 } 2683 } 2684 2685 if ($gradeavailable) { 2686 $url = new moodle_url('/grade/report/index.php', array('id' => $usercourse->id)); 2687 $usercoursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, null, 2688 new pix_icon('i/grades', '')); 2689 } 2690 2691 // Add a node to view the users notes if permitted. 2692 if (!empty($CFG->enablenotes) && 2693 has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $usercoursecontext)) { 2694 $url = new moodle_url('/notes/index.php', array('user' => $user->id, 'course' => $usercourse->id)); 2695 $usercoursenode->add(get_string('notes', 'notes'), $url, self::TYPE_SETTING); 2696 } 2697 2698 if (can_access_course($usercourse, $user->id, '', true)) { 2699 $usercoursenode->add(get_string('entercourse'), new moodle_url('/course/view.php', 2700 array('id' => $usercourse->id)), self::TYPE_SETTING, null, null, new pix_icon('i/course', '')); 2701 } 2702 2703 $reporttab = $usercoursenode->add(get_string('activityreports')); 2704 2705 $reportfunctions = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php'); 2706 foreach ($reportfunctions as $reportfunction) { 2707 $reportfunction($reporttab, $user, $usercourse); 2708 } 2709 2710 $reporttab->trim_if_empty(); 2711 } 2712 } 2713 2714 // Let plugins hook into user navigation. 2715 $pluginsfunction = get_plugins_with_function('extend_navigation_user', 'lib.php'); 2716 foreach ($pluginsfunction as $plugintype => $plugins) { 2717 if ($plugintype != 'report') { 2718 foreach ($plugins as $pluginfunction) { 2719 $pluginfunction($usernode, $user, $usercontext, $course, $coursecontext); 2720 } 2721 } 2722 } 2723 } 2724 return true; 2725 } 2726 2727 /** 2728 * This method simply checks to see if a given module can extend the navigation. 2729 * 2730 * @todo (MDL-25290) A shared caching solution should be used to save details on what extends navigation. 2731 * 2732 * @param string $modname 2733 * @return bool 2734 */ 2735 public static function module_extends_navigation($modname) { 2736 global $CFG; 2737 static $extendingmodules = array(); 2738 if (!array_key_exists($modname, $extendingmodules)) { 2739 $extendingmodules[$modname] = false; 2740 $file = $CFG->dirroot.'/mod/'.$modname.'/lib.php'; 2741 if (file_exists($file)) { 2742 $function = $modname.'_extend_navigation'; 2743 require_once($file); 2744 $extendingmodules[$modname] = (function_exists($function)); 2745 } 2746 } 2747 return $extendingmodules[$modname]; 2748 } 2749 /** 2750 * Extends the navigation for the given user. 2751 * 2752 * @param stdClass $user A user from the database 2753 */ 2754 public function extend_for_user($user) { 2755 $this->extendforuser[] = $user; 2756 } 2757 2758 /** 2759 * Returns all of the users the navigation is being extended for 2760 * 2761 * @return array An array of extending users. 2762 */ 2763 public function get_extending_users() { 2764 return $this->extendforuser; 2765 } 2766 /** 2767 * Adds the given course to the navigation structure. 2768 * 2769 * @param stdClass $course 2770 * @param bool $forcegeneric 2771 * @param bool $ismycourse 2772 * @return navigation_node 2773 */ 2774 public function add_course(stdClass $course, $forcegeneric = false, $coursetype = self::COURSE_OTHER) { 2775 global $CFG, $SITE; 2776 2777 // We found the course... we can return it now :) 2778 if (!$forcegeneric && array_key_exists($course->id, $this->addedcourses)) { 2779 return $this->addedcourses[$course->id]; 2780 } 2781 2782 $coursecontext = context_course::instance($course->id); 2783 2784 if ($coursetype != self::COURSE_MY && $coursetype != self::COURSE_CURRENT && $course->id != $SITE->id) { 2785 if (is_role_switched($course->id)) { 2786 // user has to be able to access course in order to switch, let's skip the visibility test here 2787 } else if (!core_course_category::can_view_course_info($course)) { 2788 return false; 2789 } 2790 } 2791 2792 $issite = ($course->id == $SITE->id); 2793 $displaycontext = \context_helper::get_navigation_filter_context($coursecontext); 2794 $shortname = format_string($course->shortname, true, ['context' => $displaycontext]); 2795 $fullname = format_string($course->fullname, true, ['context' => $displaycontext]); 2796 // This is the name that will be shown for the course. 2797 $coursename = empty($CFG->navshowfullcoursenames) ? $shortname : $fullname; 2798 2799 if ($coursetype == self::COURSE_CURRENT) { 2800 if ($coursenode = $this->rootnodes['mycourses']->find($course->id, self::TYPE_COURSE)) { 2801 return $coursenode; 2802 } else { 2803 $coursetype = self::COURSE_OTHER; 2804 } 2805 } 2806 2807 // Can the user expand the course to see its content. 2808 $canexpandcourse = true; 2809 if ($issite) { 2810 $parent = $this; 2811 $url = null; 2812 if (empty($CFG->usesitenameforsitepages)) { 2813 $coursename = get_string('sitepages'); 2814 } 2815 } else if ($coursetype == self::COURSE_CURRENT) { 2816 $parent = $this->rootnodes['currentcourse']; 2817 $url = new moodle_url('/course/view.php', array('id'=>$course->id)); 2818 $canexpandcourse = $this->can_expand_course($course); 2819 } else if ($coursetype == self::COURSE_MY && !$forcegeneric) { 2820 if (!empty($CFG->navshowmycoursecategories) && ($parent = $this->rootnodes['mycourses']->find($course->category, self::TYPE_MY_CATEGORY))) { 2821 // Nothing to do here the above statement set $parent to the category within mycourses. 2822 } else { 2823 $parent = $this->rootnodes['mycourses']; 2824 } 2825 $url = new moodle_url('/course/view.php', array('id'=>$course->id)); 2826 } else { 2827 $parent = $this->rootnodes['courses']; 2828 $url = new moodle_url('/course/view.php', array('id'=>$course->id)); 2829 // They can only expand the course if they can access it. 2830 $canexpandcourse = $this->can_expand_course($course); 2831 if (!empty($course->category) && $this->show_categories($coursetype == self::COURSE_MY)) { 2832 if (!$this->is_category_fully_loaded($course->category)) { 2833 // We need to load the category structure for this course 2834 $this->load_all_categories($course->category, false); 2835 } 2836 if (array_key_exists($course->category, $this->addedcategories)) { 2837 $parent = $this->addedcategories[$course->category]; 2838 // This could lead to the course being created so we should check whether it is the case again 2839 if (!$forcegeneric && array_key_exists($course->id, $this->addedcourses)) { 2840 return $this->addedcourses[$course->id]; 2841 } 2842 } 2843 } 2844 } 2845 2846 $coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id, new pix_icon('i/course', '')); 2847 $coursenode->showinflatnavigation = $coursetype == self::COURSE_MY; 2848 2849 $coursenode->hidden = (!$course->visible); 2850 $coursenode->title(format_string($course->fullname, true, ['context' => $displaycontext, 'escape' => false])); 2851 if ($canexpandcourse) { 2852 // This course can be expanded by the user, make it a branch to make the system aware that its expandable by ajax. 2853 $coursenode->nodetype = self::NODETYPE_BRANCH; 2854 $coursenode->isexpandable = true; 2855 } else { 2856 $coursenode->nodetype = self::NODETYPE_LEAF; 2857 $coursenode->isexpandable = false; 2858 } 2859 if (!$forcegeneric) { 2860 $this->addedcourses[$course->id] = $coursenode; 2861 } 2862 2863 return $coursenode; 2864 } 2865 2866 /** 2867 * Returns a cache instance to use for the expand course cache. 2868 * @return cache_session 2869 */ 2870 protected function get_expand_course_cache() { 2871 if ($this->cacheexpandcourse === null) { 2872 $this->cacheexpandcourse = cache::make('core', 'navigation_expandcourse'); 2873 } 2874 return $this->cacheexpandcourse; 2875 } 2876 2877 /** 2878 * Checks if a user can expand a course in the navigation. 2879 * 2880 * We use a cache here because in order to be accurate we need to call can_access_course which is a costly function. 2881 * Because this functionality is basic + non-essential and because we lack good event triggering this cache 2882 * permits stale data. 2883 * In the situation the user is granted access to a course after we've initialised this session cache the cache 2884 * will be stale. 2885 * It is brought up to date in only one of two ways. 2886 * 1. The user logs out and in again. 2887 * 2. The user browses to the course they've just being given access to. 2888 * 2889 * Really all this controls is whether the node is shown as expandable or not. It is uber un-important. 2890 * 2891 * @param stdClass $course 2892 * @return bool 2893 */ 2894 protected function can_expand_course($course) { 2895 $cache = $this->get_expand_course_cache(); 2896 $canexpand = $cache->get($course->id); 2897 if ($canexpand === false) { 2898 $canexpand = isloggedin() && can_access_course($course, null, '', true); 2899 $canexpand = (int)$canexpand; 2900 $cache->set($course->id, $canexpand); 2901 } 2902 return ($canexpand === 1); 2903 } 2904 2905 /** 2906 * Returns true if the category has already been loaded as have any child categories 2907 * 2908 * @param int $categoryid 2909 * @return bool 2910 */ 2911 protected function is_category_fully_loaded($categoryid) { 2912 return (array_key_exists($categoryid, $this->addedcategories) && ($this->allcategoriesloaded || $this->addedcategories[$categoryid]->children->count() > 0)); 2913 } 2914 2915 /** 2916 * Adds essential course nodes to the navigation for the given course. 2917 * 2918 * This method adds nodes such as reports, blogs and participants 2919 * 2920 * @param navigation_node $coursenode 2921 * @param stdClass $course 2922 * @return bool returns true on successful addition of a node. 2923 */ 2924 public function add_course_essentials($coursenode, stdClass $course) { 2925 global $CFG, $SITE; 2926 require_once($CFG->dirroot . '/course/lib.php'); 2927 2928 if ($course->id == $SITE->id) { 2929 return $this->add_front_page_course_essentials($coursenode, $course); 2930 } 2931 2932 if ($coursenode == false || !($coursenode instanceof navigation_node) || $coursenode->get('participants', navigation_node::TYPE_CONTAINER)) { 2933 return true; 2934 } 2935 2936 $navoptions = course_get_user_navigation_options($this->page->context, $course); 2937 2938 //Participants 2939 if ($navoptions->participants) { 2940 $participants = $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), 2941 self::TYPE_CONTAINER, get_string('participants'), 'participants', new pix_icon('i/users', '')); 2942 2943 if ($navoptions->blogs) { 2944 $blogsurls = new moodle_url('/blog/index.php'); 2945 if ($currentgroup = groups_get_course_group($course, true)) { 2946 $blogsurls->param('groupid', $currentgroup); 2947 } else { 2948 $blogsurls->param('courseid', $course->id); 2949 } 2950 $participants->add(get_string('blogscourse', 'blog'), $blogsurls->out(), self::TYPE_SETTING, null, 'courseblogs'); 2951 } 2952 2953 if ($navoptions->notes) { 2954 $participants->add(get_string('notes', 'notes'), new moodle_url('/notes/index.php', array('filtertype' => 'course', 'filterselect' => $course->id)), self::TYPE_SETTING, null, 'currentcoursenotes'); 2955 } 2956 } else if (count($this->extendforuser) > 0) { 2957 $coursenode->add(get_string('participants'), null, self::TYPE_CONTAINER, get_string('participants'), 'participants'); 2958 } 2959 2960 // Badges. 2961 if ($navoptions->badges) { 2962 $url = new moodle_url('/badges/view.php', array('type' => 2, 'id' => $course->id)); 2963 2964 $coursenode->add(get_string('coursebadges', 'badges'), $url, 2965 navigation_node::TYPE_SETTING, null, 'badgesview', 2966 new pix_icon('i/badge', get_string('coursebadges', 'badges'))); 2967 } 2968 2969 // Check access to the course and competencies page. 2970 if ($navoptions->competencies) { 2971 // Just a link to course competency. 2972 $title = get_string('competencies', 'core_competency'); 2973 $path = new moodle_url("/admin/tool/lp/coursecompetencies.php", array('courseid' => $course->id)); 2974 $coursenode->add($title, $path, navigation_node::TYPE_SETTING, null, 'competencies', 2975 new pix_icon('i/competencies', '')); 2976 } 2977 if ($navoptions->grades) { 2978 $url = new moodle_url('/grade/report/index.php', array('id'=>$course->id)); 2979 $gradenode = $coursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, 2980 'grades', new pix_icon('i/grades', '')); 2981 // If the page type matches the grade part, then make the nav drawer grade node (incl. all sub pages) active. 2982 if ($this->page->context->contextlevel < CONTEXT_MODULE && strpos($this->page->pagetype, 'grade-') === 0) { 2983 $gradenode->make_active(); 2984 } 2985 } 2986 2987 return true; 2988 } 2989 /** 2990 * This generates the structure of the course that won't be generated when 2991 * the modules and sections are added. 2992 * 2993 * Things such as the reports branch, the participants branch, blogs... get 2994 * added to the course node by this method. 2995 * 2996 * @param navigation_node $coursenode 2997 * @param stdClass $course 2998 * @return bool True for successfull generation 2999 */ 3000 public function add_front_page_course_essentials(navigation_node $coursenode, stdClass $course) { 3001 global $CFG, $USER, $COURSE, $SITE; 3002 require_once($CFG->dirroot . '/course/lib.php'); 3003 3004 if ($coursenode == false || $coursenode->get('frontpageloaded', navigation_node::TYPE_CUSTOM)) { 3005 return true; 3006 } 3007 3008 $systemcontext = context_system::instance(); 3009 $navoptions = course_get_user_navigation_options($systemcontext, $course); 3010 3011 // Hidden node that we use to determine if the front page navigation is loaded. 3012 // This required as there are not other guaranteed nodes that may be loaded. 3013 $coursenode->add('frontpageloaded', null, self::TYPE_CUSTOM, null, 'frontpageloaded')->display = false; 3014 3015 // Add My courses to the site pages within the navigation structure so the block can read it. 3016 $coursenode->add(get_string('mycourses'), new moodle_url('/my/courses.php'), self::TYPE_CUSTOM, null, 'mycourses'); 3017 3018 // Participants. 3019 if ($navoptions->participants) { 3020 $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), 3021 self::TYPE_CUSTOM, get_string('participants'), 'participants'); 3022 } 3023 3024 // Blogs. 3025 if ($navoptions->blogs) { 3026 $blogsurls = new moodle_url('/blog/index.php'); 3027 $coursenode->add(get_string('blogssite', 'blog'), $blogsurls->out(), self::TYPE_SYSTEM, null, 'siteblog'); 3028 } 3029 3030 $filterselect = 0; 3031 3032 // Badges. 3033 if ($navoptions->badges) { 3034 $url = new moodle_url($CFG->wwwroot . '/badges/view.php', array('type' => 1)); 3035 $coursenode->add(get_string('sitebadges', 'badges'), $url, navigation_node::TYPE_CUSTOM); 3036 } 3037 3038 // Notes. 3039 if ($navoptions->notes) { 3040 $coursenode->add(get_string('notes', 'notes'), new moodle_url('/notes/index.php', 3041 array('filtertype' => 'course', 'filterselect' => $filterselect)), self::TYPE_SETTING, null, 'notes'); 3042 } 3043 3044 // Tags 3045 if ($navoptions->tags) { 3046 $node = $coursenode->add(get_string('tags', 'tag'), new moodle_url('/tag/search.php'), 3047 self::TYPE_SETTING, null, 'tags'); 3048 } 3049 3050 // Search. 3051 if ($navoptions->search) { 3052 $node = $coursenode->add(get_string('search', 'search'), new moodle_url('/search/index.php'), 3053 self::TYPE_SETTING, null, 'search'); 3054 } 3055 3056 if (isloggedin()) { 3057 $usercontext = context_user::instance($USER->id); 3058 if (has_capability('moodle/user:manageownfiles', $usercontext)) { 3059 $url = new moodle_url('/user/files.php'); 3060 $node = $coursenode->add(get_string('privatefiles'), $url, 3061 self::TYPE_SETTING, null, 'privatefiles', new pix_icon('i/privatefiles', '')); 3062 $node->display = false; 3063 $node->showinflatnavigation = true; 3064 $node->mainnavonly = true; 3065 } 3066 } 3067 3068 if (isloggedin()) { 3069 $context = $this->page->context; 3070 switch ($context->contextlevel) { 3071 case CONTEXT_COURSECAT: 3072 // OK, expected context level. 3073 break; 3074 case CONTEXT_COURSE: 3075 // OK, expected context level if not on frontpage. 3076 if ($COURSE->id != $SITE->id) { 3077 break; 3078 } 3079 default: 3080 // If this context is part of a course (excluding frontpage), use the course context. 3081 // Otherwise, use the system context. 3082 $coursecontext = $context->get_course_context(false); 3083 if ($coursecontext && $coursecontext->instanceid !== $SITE->id) { 3084 $context = $coursecontext; 3085 } else { 3086 $context = $systemcontext; 3087 } 3088 } 3089 3090 $params = ['contextid' => $context->id]; 3091 if (has_capability('moodle/contentbank:access', $context)) { 3092 $url = new moodle_url('/contentbank/index.php', $params); 3093 $node = $coursenode->add(get_string('contentbank'), $url, 3094 self::TYPE_CUSTOM, null, 'contentbank', new pix_icon('i/contentbank', '')); 3095 $node->showinflatnavigation = true; 3096 } 3097 } 3098 3099 return true; 3100 } 3101 3102 /** 3103 * Clears the navigation cache 3104 */ 3105 public function clear_cache() { 3106 $this->cache->clear(); 3107 } 3108 3109 /** 3110 * Sets an expansion limit for the navigation 3111 * 3112 * The expansion limit is used to prevent the display of content that has a type 3113 * greater than the provided $type. 3114 * 3115 * Can be used to ensure things such as activities or activity content don't get 3116 * shown on the navigation. 3117 * They are still generated in order to ensure the navbar still makes sense. 3118 * 3119 * @param int $type One of navigation_node::TYPE_* 3120 * @return bool true when complete. 3121 */ 3122 public function set_expansion_limit($type) { 3123 global $SITE; 3124 $nodes = $this->find_all_of_type($type); 3125 3126 // We only want to hide specific types of nodes. 3127 // Only nodes that represent "structure" in the navigation tree should be hidden. 3128 // If we hide all nodes then we risk hiding vital information. 3129 $typestohide = array( 3130 self::TYPE_CATEGORY, 3131 self::TYPE_COURSE, 3132 self::TYPE_SECTION, 3133 self::TYPE_ACTIVITY 3134 ); 3135 3136 foreach ($nodes as $node) { 3137 // We need to generate the full site node 3138 if ($type == self::TYPE_COURSE && $node->key == $SITE->id) { 3139 continue; 3140 } 3141 foreach ($node->children as $child) { 3142 $child->hide($typestohide); 3143 } 3144 } 3145 return true; 3146 } 3147 /** 3148 * Attempts to get the navigation with the given key from this nodes children. 3149 * 3150 * This function only looks at this nodes children, it does NOT look recursivily. 3151 * If the node can't be found then false is returned. 3152 * 3153 * If you need to search recursivily then use the {@link global_navigation::find()} method. 3154 * 3155 * Note: If you are trying to set the active node {@link navigation_node::override_active_url()} 3156 * may be of more use to you. 3157 * 3158 * @param string|int $key The key of the node you wish to receive. 3159 * @param int $type One of navigation_node::TYPE_* 3160 * @return navigation_node|false 3161 */ 3162 public function get($key, $type = null) { 3163 if (!$this->initialised) { 3164 $this->initialise(); 3165 } 3166 return parent::get($key, $type); 3167 } 3168 3169 /** 3170 * Searches this nodes children and their children to find a navigation node 3171 * with the matching key and type. 3172 * 3173 * This method is recursive and searches children so until either a node is 3174 * found or there are no more nodes to search. 3175 * 3176 * If you know that the node being searched for is a child of this node 3177 * then use the {@link global_navigation::get()} method instead. 3178 * 3179 * Note: If you are trying to set the active node {@link navigation_node::override_active_url()} 3180 * may be of more use to you. 3181 * 3182 * @param string|int $key The key of the node you wish to receive. 3183 * @param int $type One of navigation_node::TYPE_* 3184 * @return navigation_node|false 3185 */ 3186 public function find($key, $type) { 3187 if (!$this->initialised) { 3188 $this->initialise(); 3189 } 3190 if ($type == self::TYPE_ROOTNODE && array_key_exists($key, $this->rootnodes)) { 3191 return $this->rootnodes[$key]; 3192 } 3193 return parent::find($key, $type); 3194 } 3195 3196 /** 3197 * They've expanded the 'my courses' branch. 3198 */ 3199 protected function load_courses_enrolled() { 3200 global $CFG; 3201 3202 $limit = (int) $CFG->navcourselimit; 3203 3204 $courses = enrol_get_my_courses('*'); 3205 $flatnavcourses = []; 3206 3207 // Go through the courses and see which ones we want to display in the flatnav. 3208 foreach ($courses as $course) { 3209 $classify = course_classify_for_timeline($course); 3210 3211 if ($classify == COURSE_TIMELINE_INPROGRESS) { 3212 $flatnavcourses[$course->id] = $course; 3213 } 3214 } 3215 3216 // Get the number of courses that can be displayed in the nav block and in the flatnav. 3217 $numtotalcourses = count($courses); 3218 $numtotalflatnavcourses = count($flatnavcourses); 3219 3220 // Reduce the size of the arrays to abide by the 'navcourselimit' setting. 3221 $courses = array_slice($courses, 0, $limit, true); 3222 $flatnavcourses = array_slice($flatnavcourses, 0, $limit, true); 3223 3224 // Get the number of courses we are going to show for each. 3225 $numshowncourses = count($courses); 3226 $numshownflatnavcourses = count($flatnavcourses); 3227 if ($numshowncourses && $this->show_my_categories()) { 3228 // Generate an array containing unique values of all the courses' categories. 3229 $categoryids = array(); 3230 foreach ($courses as $course) { 3231 if (in_array($course->category, $categoryids)) { 3232 continue; 3233 } 3234 $categoryids[] = $course->category; 3235 } 3236 3237 // Array of category IDs that include the categories of the user's courses and the related course categories. 3238 $fullpathcategoryids = []; 3239 // Get the course categories for the enrolled courses' category IDs. 3240 $mycoursecategories = core_course_category::get_many($categoryids); 3241 // Loop over each of these categories and build the category tree using each category's path. 3242 foreach ($mycoursecategories as $mycoursecat) { 3243 $pathcategoryids = explode('/', $mycoursecat->path); 3244 // First element of the exploded path is empty since paths begin with '/'. 3245 array_shift($pathcategoryids); 3246 // Merge the exploded category IDs into the full list of category IDs that we will fetch. 3247 $fullpathcategoryids = array_merge($fullpathcategoryids, $pathcategoryids); 3248 } 3249 3250 // Fetch all of the categories related to the user's courses. 3251 $pathcategories = core_course_category::get_many($fullpathcategoryids); 3252 // Loop over each of these categories and build the category tree. 3253 foreach ($pathcategories as $coursecat) { 3254 // No need to process categories that have already been added. 3255 if (isset($this->addedcategories[$coursecat->id])) { 3256 continue; 3257 } 3258 // Skip categories that are not visible. 3259 if (!$coursecat->is_uservisible()) { 3260 continue; 3261 } 3262 3263 // Get this course category's parent node. 3264 $parent = null; 3265 if ($coursecat->parent && isset($this->addedcategories[$coursecat->parent])) { 3266 $parent = $this->addedcategories[$coursecat->parent]; 3267 } 3268 if (!$parent) { 3269 // If it has no parent, then it should be right under the My courses node. 3270 $parent = $this->rootnodes['mycourses']; 3271 } 3272 3273 // Build the category object based from the coursecat object. 3274 $mycategory = new stdClass(); 3275 $mycategory->id = $coursecat->id; 3276 $mycategory->name = $coursecat->name; 3277 $mycategory->visible = $coursecat->visible; 3278 3279 // Add this category to the nav tree. 3280 $this->add_category($mycategory, $parent, self::TYPE_MY_CATEGORY); 3281 } 3282 } 3283 3284 // Go through each course now and add it to the nav block, and the flatnav if applicable. 3285 foreach ($courses as $course) { 3286 $node = $this->add_course($course, false, self::COURSE_MY); 3287 if ($node) { 3288 $node->showinflatnavigation = false; 3289 // Check if we should also add this to the flat nav as well. 3290 if (isset($flatnavcourses[$course->id])) { 3291 $node->showinflatnavigation = true; 3292 } 3293 } 3294 } 3295 3296 // Go through each course in the flatnav now. 3297 foreach ($flatnavcourses as $course) { 3298 // Check if we haven't already added it. 3299 if (!isset($courses[$course->id])) { 3300 // Ok, add it to the flatnav only. 3301 $node = $this->add_course($course, false, self::COURSE_MY); 3302 $node->display = false; 3303 $node->showinflatnavigation = true; 3304 } 3305 } 3306 3307 $showmorelinkinnav = $numtotalcourses > $numshowncourses; 3308 $showmorelinkinflatnav = $numtotalflatnavcourses > $numshownflatnavcourses; 3309 // Show a link to the course page if there are more courses the user is enrolled in. 3310 if ($showmorelinkinnav || $showmorelinkinflatnav) { 3311 // Adding hash to URL so the link is not highlighted in the navigation when clicked. 3312 $url = new moodle_url('/my/courses.php'); 3313 $parent = $this->rootnodes['mycourses']; 3314 $coursenode = $parent->add(get_string('morenavigationlinks'), $url, self::TYPE_CUSTOM, null, self::COURSE_INDEX_PAGE); 3315 3316 if ($showmorelinkinnav) { 3317 $coursenode->display = true; 3318 } 3319 3320 if ($showmorelinkinflatnav) { 3321 $coursenode->showinflatnavigation = true; 3322 } 3323 } 3324 } 3325 } 3326 3327 /** 3328 * The global navigation class used especially for AJAX requests. 3329 * 3330 * The primary methods that are used in the global navigation class have been overriden 3331 * to ensure that only the relevant branch is generated at the root of the tree. 3332 * This can be done because AJAX is only used when the backwards structure for the 3333 * requested branch exists. 3334 * This has been done only because it shortens the amounts of information that is generated 3335 * which of course will speed up the response time.. because no one likes laggy AJAX. 3336 * 3337 * @package core 3338 * @category navigation 3339 * @copyright 2009 Sam Hemelryk 3340 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 3341 */ 3342 class global_navigation_for_ajax extends global_navigation { 3343 3344 /** @var int used for determining what type of navigation_node::TYPE_* is being used */ 3345 protected $branchtype; 3346 3347 /** @var int the instance id */ 3348 protected $instanceid; 3349 3350 /** @var array Holds an array of expandable nodes */ 3351 protected $expandable = array(); 3352 3353 /** 3354 * Constructs the navigation for use in an AJAX request 3355 * 3356 * @param moodle_page $page moodle_page object 3357 * @param int $branchtype 3358 * @param int $id 3359 */ 3360 public function __construct($page, $branchtype, $id) { 3361 $this->page = $page; 3362 $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME); 3363 $this->children = new navigation_node_collection(); 3364 $this->branchtype = $branchtype; 3365 $this->instanceid = $id; 3366 $this->initialise(); 3367 } 3368 /** 3369 * Initialise the navigation given the type and id for the branch to expand. 3370 * 3371 * @return array An array of the expandable nodes 3372 */ 3373 public function initialise() { 3374 global $DB, $SITE; 3375 3376 if ($this->initialised || during_initial_install()) { 3377 return $this->expandable; 3378 } 3379 $this->initialised = true; 3380 3381 $this->rootnodes = array(); 3382 $this->rootnodes['site'] = $this->add_course($SITE); 3383 $this->rootnodes['mycourses'] = $this->add( 3384 get_string('mycourses'), 3385 new moodle_url('/my/courses.php'), 3386 self::TYPE_ROOTNODE, 3387 null, 3388 'mycourses' 3389 ); 3390 $this->rootnodes['courses'] = $this->add(get_string('courses'), null, self::TYPE_ROOTNODE, null, 'courses'); 3391 // The courses branch is always displayed, and is always expandable (although may be empty). 3392 // This mimicks what is done during {@link global_navigation::initialise()}. 3393 $this->rootnodes['courses']->isexpandable = true; 3394 3395 // Branchtype will be one of navigation_node::TYPE_* 3396 switch ($this->branchtype) { 3397 case 0: 3398 if ($this->instanceid === 'mycourses') { 3399 $this->load_courses_enrolled(); 3400 } else if ($this->instanceid === 'courses') { 3401 $this->load_courses_other(); 3402 } 3403 break; 3404 case self::TYPE_CATEGORY : 3405 $this->load_category($this->instanceid); 3406 break; 3407 case self::TYPE_MY_CATEGORY : 3408 $this->load_category($this->instanceid, self::TYPE_MY_CATEGORY); 3409 break; 3410 case self::TYPE_COURSE : 3411 $course = $DB->get_record('course', array('id' => $this->instanceid), '*', MUST_EXIST); 3412 if (!can_access_course($course, null, '', true)) { 3413 // Thats OK all courses are expandable by default. We don't need to actually expand it we can just 3414 // add the course node and break. This leads to an empty node. 3415 $this->add_course($course); 3416 break; 3417 } 3418 require_course_login($course, true, null, false, true); 3419 $this->page->set_context(context_course::instance($course->id)); 3420 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 3421 $this->add_course_essentials($coursenode, $course); 3422 $this->load_course_sections($course, $coursenode); 3423 break; 3424 case self::TYPE_SECTION : 3425 $sql = 'SELECT c.*, cs.section AS sectionnumber 3426 FROM {course} c 3427 LEFT JOIN {course_sections} cs ON cs.course = c.id 3428 WHERE cs.id = ?'; 3429 $course = $DB->get_record_sql($sql, array($this->instanceid), MUST_EXIST); 3430 require_course_login($course, true, null, false, true); 3431 $this->page->set_context(context_course::instance($course->id)); 3432 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 3433 $this->add_course_essentials($coursenode, $course); 3434 $this->load_course_sections($course, $coursenode, $course->sectionnumber); 3435 break; 3436 case self::TYPE_ACTIVITY : 3437 $sql = "SELECT c.* 3438 FROM {course} c 3439 JOIN {course_modules} cm ON cm.course = c.id 3440 WHERE cm.id = :cmid"; 3441 $params = array('cmid' => $this->instanceid); 3442 $course = $DB->get_record_sql($sql, $params, MUST_EXIST); 3443 $modinfo = get_fast_modinfo($course); 3444 $cm = $modinfo->get_cm($this->instanceid); 3445 require_course_login($course, true, $cm, false, true); 3446 $this->page->set_context(context_module::instance($cm->id)); 3447 $coursenode = $this->add_course($course, false, self::COURSE_CURRENT); 3448 $this->load_course_sections($course, $coursenode, null, $cm); 3449 $activitynode = $coursenode->find($cm->id, self::TYPE_ACTIVITY); 3450 if ($activitynode) { 3451 $modulenode = $this->load_activity($cm, $course, $activitynode); 3452 } 3453 break; 3454 default: 3455 throw new Exception('Unknown type'); 3456 return $this->expandable; 3457 } 3458 3459 if ($this->page->context->contextlevel == CONTEXT_COURSE && $this->page->context->instanceid != $SITE->id) { 3460 $this->load_for_user(null, true); 3461 } 3462 3463 // Give the local plugins a chance to include some navigation if they want. 3464 $this->load_local_plugin_navigation(); 3465 3466 $this->find_expandable($this->expandable); 3467 return $this->expandable; 3468 } 3469 3470 /** 3471 * They've expanded the general 'courses' branch. 3472 */ 3473 protected function load_courses_other() { 3474 $this->load_all_courses(); 3475 } 3476 3477 /** 3478 * Loads a single category into the AJAX navigation. 3479 * 3480 * This function is special in that it doesn't concern itself with the parent of 3481 * the requested category or its siblings. 3482 * This is because with the AJAX navigation we know exactly what is wanted and only need to 3483 * request that. 3484 * 3485 * @global moodle_database $DB 3486 * @param int $categoryid id of category to load in navigation. 3487 * @param int $nodetype type of node, if category is under MyHome then it's TYPE_MY_CATEGORY 3488 * @return void. 3489 */ 3490 protected function load_category($categoryid, $nodetype = self::TYPE_CATEGORY) { 3491 global $CFG, $DB; 3492 3493 $limit = 20; 3494 if (!empty($CFG->navcourselimit)) { 3495 $limit = (int)$CFG->navcourselimit; 3496 } 3497 3498 $catcontextsql = context_helper::get_preload_record_columns_sql('ctx'); 3499 $sql = "SELECT cc.*, $catcontextsql 3500 FROM {course_categories} cc 3501 JOIN {context} ctx ON cc.id = ctx.instanceid 3502 WHERE ctx.contextlevel = ".CONTEXT_COURSECAT." AND 3503 (cc.id = :categoryid1 OR cc.parent = :categoryid2) 3504 ORDER BY cc.depth ASC, cc.sortorder ASC, cc.id ASC"; 3505 $params = array('categoryid1' => $categoryid, 'categoryid2' => $categoryid); 3506 $categories = $DB->get_recordset_sql($sql, $params, 0, $limit); 3507 $categorylist = array(); 3508 $subcategories = array(); 3509 $basecategory = null; 3510 foreach ($categories as $category) { 3511 $categorylist[] = $category->id; 3512 context_helper::preload_from_record($category); 3513 if ($category->id == $categoryid) { 3514 $this->add_category($category, $this, $nodetype); 3515 $basecategory = $this->addedcategories[$category->id]; 3516 } else { 3517 $subcategories[$category->id] = $category; 3518 } 3519 } 3520 $categories->close(); 3521 3522 3523 // If category is shown in MyHome then only show enrolled courses and hide empty subcategories, 3524 // else show all courses. 3525 if ($nodetype === self::TYPE_MY_CATEGORY) { 3526 $courses = enrol_get_my_courses('*'); 3527 $categoryids = array(); 3528 3529 // Only search for categories if basecategory was found. 3530 if (!is_null($basecategory)) { 3531 // Get course parent category ids. 3532 foreach ($courses as $course) { 3533 $categoryids[] = $course->category; 3534 } 3535 3536 // Get a unique list of category ids which a part of the path 3537 // to user's courses. 3538 $coursesubcategories = array(); 3539 $addedsubcategories = array(); 3540 3541 list($sql, $params) = $DB->get_in_or_equal($categoryids); 3542 $categories = $DB->get_recordset_select('course_categories', 'id '.$sql, $params, 'sortorder, id', 'id, path'); 3543 3544 foreach ($categories as $category){ 3545 $coursesubcategories = array_merge($coursesubcategories, explode('/', trim($category->path, "/"))); 3546 } 3547 $categories->close(); 3548 $coursesubcategories = array_unique($coursesubcategories); 3549 3550 // Only add a subcategory if it is part of the path to user's course and 3551 // wasn't already added. 3552 foreach ($subcategories as $subid => $subcategory) { 3553 if (in_array($subid, $coursesubcategories) && 3554 !in_array($subid, $addedsubcategories)) { 3555 $this->add_category($subcategory, $basecategory, $nodetype); 3556 $addedsubcategories[] = $subid; 3557 } 3558 } 3559 } 3560 3561 foreach ($courses as $course) { 3562 // Add course if it's in category. 3563 if (in_array($course->category, $categorylist)) { 3564 $this->add_course($course, true, self::COURSE_MY); 3565 } 3566 } 3567 } else { 3568 if (!is_null($basecategory)) { 3569 foreach ($subcategories as $key=>$category) { 3570 $this->add_category($category, $basecategory, $nodetype); 3571 } 3572 } 3573 $courses = $DB->get_recordset('course', array('category' => $categoryid), 'sortorder', '*' , 0, $limit); 3574 foreach ($courses as $course) { 3575 $this->add_course($course); 3576 } 3577 $courses->close(); 3578 } 3579 } 3580 3581 /** 3582 * Returns an array of expandable nodes 3583 * @return array 3584 */ 3585 public function get_expandable() { 3586 return $this->expandable; 3587 } 3588 } 3589 3590 /** 3591 * Navbar class 3592 * 3593 * This class is used to manage the navbar, which is initialised from the navigation 3594 * object held by PAGE 3595 * 3596 * @package core 3597 * @category navigation 3598 * @copyright 2009 Sam Hemelryk 3599 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 3600 */ 3601 class navbar extends navigation_node { 3602 /** @var bool A switch for whether the navbar is initialised or not */ 3603 protected $initialised = false; 3604 /** @var mixed keys used to reference the nodes on the navbar */ 3605 protected $keys = array(); 3606 /** @var null|string content of the navbar */ 3607 protected $content = null; 3608 /** @var moodle_page object the moodle page that this navbar belongs to */ 3609 protected $page; 3610 /** @var bool A switch for whether to ignore the active navigation information */ 3611 protected $ignoreactive = false; 3612 /** @var bool A switch to let us know if we are in the middle of an install */ 3613 protected $duringinstall = false; 3614 /** @var bool A switch for whether the navbar has items */ 3615 protected $hasitems = false; 3616 /** @var array An array of navigation nodes for the navbar */ 3617 protected $items; 3618 /** @var array An array of child node objects */ 3619 public $children = array(); 3620 /** @var bool A switch for whether we want to include the root node in the navbar */ 3621 public $includesettingsbase = false; 3622 /** @var breadcrumb_navigation_node[] $prependchildren */ 3623 protected $prependchildren = array(); 3624 3625 /** 3626 * The almighty constructor 3627 * 3628 * @param moodle_page $page 3629 */ 3630 public function __construct(moodle_page $page) { 3631 global $CFG; 3632 if (during_initial_install()) { 3633 $this->duringinstall = true; 3634 return false; 3635 } 3636 $this->page = $page; 3637 $this->text = get_string('home'); 3638 $this->shorttext = get_string('home'); 3639 $this->action = new moodle_url($CFG->wwwroot); 3640 $this->nodetype = self::NODETYPE_BRANCH; 3641 $this->type = self::TYPE_SYSTEM; 3642 } 3643 3644 /** 3645 * Quick check to see if the navbar will have items in. 3646 * 3647 * @return bool Returns true if the navbar will have items, false otherwise 3648 */ 3649 public function has_items() { 3650 if ($this->duringinstall) { 3651 return false; 3652 } else if ($this->hasitems !== false) { 3653 return true; 3654 } 3655 $outcome = false; 3656 if (count($this->children) > 0 || count($this->prependchildren) > 0) { 3657 // There have been manually added items - there are definitely items. 3658 $outcome = true; 3659 } else if (!$this->ignoreactive) { 3660 // We will need to initialise the navigation structure to check if there are active items. 3661 $this->page->navigation->initialise($this->page); 3662 $outcome = ($this->page->navigation->contains_active_node() || $this->page->settingsnav->contains_active_node()); 3663 } 3664 $this->hasitems = $outcome; 3665 return $outcome; 3666 } 3667 3668 /** 3669 * Turn on/off ignore active 3670 * 3671 * @param bool $setting 3672 */ 3673 public function ignore_active($setting=true) { 3674 $this->ignoreactive = ($setting); 3675 } 3676 3677 /** 3678 * Gets a navigation node 3679 * 3680 * @param string|int $key for referencing the navbar nodes 3681 * @param int $type breadcrumb_navigation_node::TYPE_* 3682 * @return breadcrumb_navigation_node|bool 3683 */ 3684 public function get($key, $type = null) { 3685 foreach ($this->children as &$child) { 3686 if ($child->key === $key && ($type == null || $type == $child->type)) { 3687 return $child; 3688 } 3689 } 3690 foreach ($this->prependchildren as &$child) { 3691 if ($child->key === $key && ($type == null || $type == $child->type)) { 3692 return $child; 3693 } 3694 } 3695 return false; 3696 } 3697 /** 3698 * Returns an array of breadcrumb_navigation_nodes that make up the navbar. 3699 * 3700 * @return array 3701 */ 3702 public function get_items() { 3703 global $CFG; 3704 $items = array(); 3705 // Make sure that navigation is initialised 3706 if (!$this->has_items()) { 3707 return $items; 3708 } 3709 if ($this->items !== null) { 3710 return $this->items; 3711 } 3712 3713 if (count($this->children) > 0) { 3714 // Add the custom children. 3715 $items = array_reverse($this->children); 3716 } 3717 3718 // Check if navigation contains the active node 3719 if (!$this->ignoreactive) { 3720 // We will need to ensure the navigation has been initialised. 3721 $this->page->navigation->initialise($this->page); 3722 // Now find the active nodes on both the navigation and settings. 3723 $navigationactivenode = $this->page->navigation->find_active_node(); 3724 $settingsactivenode = $this->page->settingsnav->find_active_node(); 3725 3726 if ($navigationactivenode && $settingsactivenode) { 3727 // Parse a combined navigation tree 3728 while ($settingsactivenode && $settingsactivenode->parent !== null) { 3729 if (!$settingsactivenode->mainnavonly) { 3730 $items[] = new breadcrumb_navigation_node($settingsactivenode); 3731 } 3732 $settingsactivenode = $settingsactivenode->parent; 3733 } 3734 if (!$this->includesettingsbase) { 3735 // Removes the first node from the settings (root node) from the list 3736 array_pop($items); 3737 } 3738 while ($navigationactivenode && $navigationactivenode->parent !== null) { 3739 if (!$navigationactivenode->mainnavonly) { 3740 $items[] = new breadcrumb_navigation_node($navigationactivenode); 3741 } 3742 if (!empty($CFG->navshowcategories) && 3743 $navigationactivenode->type === self::TYPE_COURSE && 3744 $navigationactivenode->parent->key === 'currentcourse') { 3745 foreach ($this->get_course_categories() as $item) { 3746 $items[] = new breadcrumb_navigation_node($item); 3747 } 3748 } 3749 $navigationactivenode = $navigationactivenode->parent; 3750 } 3751 } else if ($navigationactivenode) { 3752 // Parse the navigation tree to get the active node 3753 while ($navigationactivenode && $navigationactivenode->parent !== null) { 3754 if (!$navigationactivenode->mainnavonly) { 3755 $items[] = new breadcrumb_navigation_node($navigationactivenode); 3756 } 3757 if (!empty($CFG->navshowcategories) && 3758 $navigationactivenode->type === self::TYPE_COURSE && 3759 $navigationactivenode->parent->key === 'currentcourse') { 3760 foreach ($this->get_course_categories() as $item) { 3761 $items[] = new breadcrumb_navigation_node($item); 3762 } 3763 } 3764 $navigationactivenode = $navigationactivenode->parent; 3765 } 3766 } else if ($settingsactivenode) { 3767 // Parse the settings navigation to get the active node 3768 while ($settingsactivenode && $settingsactivenode->parent !== null) { 3769 if (!$settingsactivenode->mainnavonly) { 3770 $items[] = new breadcrumb_navigation_node($settingsactivenode); 3771 } 3772 $settingsactivenode = $settingsactivenode->parent; 3773 } 3774 } 3775 } 3776 3777 $items[] = new breadcrumb_navigation_node(array( 3778 'text' => $this->page->navigation->text, 3779 'shorttext' => $this->page->navigation->shorttext, 3780 'key' => $this->page->navigation->key, 3781 'action' => $this->page->navigation->action 3782 )); 3783 3784 if (count($this->prependchildren) > 0) { 3785 // Add the custom children 3786 $items = array_merge($items, array_reverse($this->prependchildren)); 3787 } 3788 3789 $last = reset($items); 3790 if ($last) { 3791 $last->set_last(true); 3792 } 3793 $this->items = array_reverse($items); 3794 return $this->items; 3795 } 3796 3797 /** 3798 * Get the list of categories leading to this course. 3799 * 3800 * This function is used by {@link navbar::get_items()} to add back the "courses" 3801 * node and category chain leading to the current course. Note that this is only ever 3802 * called for the current course, so we don't need to bother taking in any parameters. 3803 * 3804 * @return array 3805 */ 3806 private function get_course_categories() { 3807 global $CFG; 3808 require_once($CFG->dirroot.'/course/lib.php'); 3809 3810 $categories = array(); 3811 $cap = 'moodle/category:viewhiddencategories'; 3812 $showcategories = !core_course_category::is_simple_site(); 3813 3814 if ($showcategories) { 3815 foreach ($this->page->categories as $category) { 3816 $context = context_coursecat::instance($category->id); 3817 if (!core_course_category::can_view_category($category)) { 3818 continue; 3819 } 3820 3821 $displaycontext = \context_helper::get_navigation_filter_context($context); 3822 $url = new moodle_url('/course/index.php', ['categoryid' => $category->id]); 3823 $name = format_string($category->name, true, ['context' => $displaycontext]); 3824 $categorynode = breadcrumb_navigation_node::create($name, $url, self::TYPE_CATEGORY, null, $category->id); 3825 if (!$category->visible) { 3826 $categorynode->hidden = true; 3827 } 3828 $categories[] = $categorynode; 3829 } 3830 } 3831 3832 // Don't show the 'course' node if enrolled in this course. 3833 $coursecontext = context_course::instance($this->page->course->id); 3834 if (!is_enrolled($coursecontext, null, '', true)) { 3835 $courses = $this->page->navigation->get('courses'); 3836 if (!$courses) { 3837 // Courses node may not be present. 3838 $courses = breadcrumb_navigation_node::create( 3839 get_string('courses'), 3840 new moodle_url('/course/index.php'), 3841 self::TYPE_CONTAINER 3842 ); 3843 } 3844 $categories[] = $courses; 3845 } 3846 3847 return $categories; 3848 } 3849 3850 /** 3851 * Add a new breadcrumb_navigation_node to the navbar, overrides parent::add 3852 * 3853 * This function overrides {@link breadcrumb_navigation_node::add()} so that we can change 3854 * the way nodes get added to allow us to simply call add and have the node added to the 3855 * end of the navbar 3856 * 3857 * @param string $text 3858 * @param string|moodle_url|action_link $action An action to associate with this node. 3859 * @param int $type One of navigation_node::TYPE_* 3860 * @param string $shorttext 3861 * @param string|int $key A key to identify this node with. Key + type is unique to a parent. 3862 * @param pix_icon $icon An optional icon to use for this node. 3863 * @return navigation_node 3864 */ 3865 public function add($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) { 3866 if ($this->content !== null) { 3867 debugging('Nav bar items must be printed before $OUTPUT->header() has been called', DEBUG_DEVELOPER); 3868 } 3869 3870 // Properties array used when creating the new navigation node 3871 $itemarray = array( 3872 'text' => $text, 3873 'type' => $type 3874 ); 3875 // Set the action if one was provided 3876 if ($action!==null) { 3877 $itemarray['action'] = $action; 3878 } 3879 // Set the shorttext if one was provided 3880 if ($shorttext!==null) { 3881 $itemarray['shorttext'] = $shorttext; 3882 } 3883 // Set the icon if one was provided 3884 if ($icon!==null) { 3885 $itemarray['icon'] = $icon; 3886 } 3887 // Default the key to the number of children if not provided 3888 if ($key === null) { 3889 $key = count($this->children); 3890 } 3891 // Set the key 3892 $itemarray['key'] = $key; 3893 // Set the parent to this node 3894 $itemarray['parent'] = $this; 3895 // Add the child using the navigation_node_collections add method 3896 $this->children[] = new breadcrumb_navigation_node($itemarray); 3897 return $this; 3898 } 3899 3900 /** 3901 * Prepends a new navigation_node to the start of the navbar 3902 * 3903 * @param string $text 3904 * @param string|moodle_url|action_link $action An action to associate with this node. 3905 * @param int $type One of navigation_node::TYPE_* 3906 * @param string $shorttext 3907 * @param string|int $key A key to identify this node with. Key + type is unique to a parent. 3908 * @param pix_icon $icon An optional icon to use for this node. 3909 * @return navigation_node 3910 */ 3911 public function prepend($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) { 3912 if ($this->content !== null) { 3913 debugging('Nav bar items must be printed before $OUTPUT->header() has been called', DEBUG_DEVELOPER); 3914 } 3915 // Properties array used when creating the new navigation node. 3916 $itemarray = array( 3917 'text' => $text, 3918 'type' => $type 3919 ); 3920 // Set the action if one was provided. 3921 if ($action!==null) { 3922 $itemarray['action'] = $action; 3923 } 3924 // Set the shorttext if one was provided. 3925 if ($shorttext!==null) { 3926 $itemarray['shorttext'] = $shorttext; 3927 } 3928 // Set the icon if one was provided. 3929 if ($icon!==null) { 3930 $itemarray['icon'] = $icon; 3931 } 3932 // Default the key to the number of children if not provided. 3933 if ($key === null) { 3934 $key = count($this->children); 3935 } 3936 // Set the key. 3937 $itemarray['key'] = $key; 3938 // Set the parent to this node. 3939 $itemarray['parent'] = $this; 3940 // Add the child node to the prepend list. 3941 $this->prependchildren[] = new breadcrumb_navigation_node($itemarray); 3942 return $this; 3943 } 3944 } 3945 3946 /** 3947 * Subclass of navigation_node allowing different rendering for the breadcrumbs 3948 * in particular adding extra metadata for search engine robots to leverage. 3949 * 3950 * @package core 3951 * @category navigation 3952 * @copyright 2015 Brendan Heywood 3953 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 3954 */ 3955 class breadcrumb_navigation_node extends navigation_node { 3956 3957 /** @var $last boolean A flag indicating this is the last item in the list of breadcrumbs. */ 3958 private $last = false; 3959 3960 /** 3961 * A proxy constructor 3962 * 3963 * @param mixed $navnode A navigation_node or an array 3964 */ 3965 public function __construct($navnode) { 3966 if (is_array($navnode)) { 3967 parent::__construct($navnode); 3968 } else if ($navnode instanceof navigation_node) { 3969 3970 // Just clone everything. 3971 $objvalues = get_object_vars($navnode); 3972 foreach ($objvalues as $key => $value) { 3973 $this->$key = $value; 3974 } 3975 } else { 3976 throw new coding_exception('Not a valid breadcrumb_navigation_node'); 3977 } 3978 } 3979 3980 /** 3981 * Getter for "last" 3982 * @return boolean 3983 */ 3984 public function is_last() { 3985 return $this->last; 3986 } 3987 3988 /** 3989 * Setter for "last" 3990 * @param $val boolean 3991 */ 3992 public function set_last($val) { 3993 $this->last = $val; 3994 } 3995 } 3996 3997 /** 3998 * Subclass of navigation_node allowing different rendering for the flat navigation 3999 * in particular allowing dividers and indents. 4000 * 4001 * @deprecated since Moodle 4.0 - do not use any more. Leverage secondary/tertiary navigation concepts 4002 * @package core 4003 * @category navigation 4004 * @copyright 2016 Damyon Wiese 4005 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 4006 */ 4007 class flat_navigation_node extends navigation_node { 4008 4009 /** @var $indent integer The indent level */ 4010 private $indent = 0; 4011 4012 /** @var $showdivider bool Show a divider before this element */ 4013 private $showdivider = false; 4014 4015 /** @var $collectionlabel string Label for a group of nodes */ 4016 private $collectionlabel = ''; 4017 4018 /** 4019 * A proxy constructor 4020 * 4021 * @param mixed $navnode A navigation_node or an array 4022 */ 4023 public function __construct($navnode, $indent) { 4024 debugging("Flat nav has been deprecated in favour of primary/secondary navigation concepts"); 4025 if (is_array($navnode)) { 4026 parent::__construct($navnode); 4027 } else if ($navnode instanceof navigation_node) { 4028 4029 // Just clone everything. 4030 $objvalues = get_object_vars($navnode); 4031 foreach ($objvalues as $key => $value) { 4032 $this->$key = $value; 4033 } 4034 } else { 4035 throw new coding_exception('Not a valid flat_navigation_node'); 4036 } 4037 $this->indent = $indent; 4038 } 4039 4040 /** 4041 * Setter, a label is required for a flat navigation node that shows a divider. 4042 * 4043 * @param string $label 4044 */ 4045 public function set_collectionlabel($label) { 4046 $this->collectionlabel = $label; 4047 } 4048 4049 /** 4050 * Getter, get the label for this flat_navigation node, or it's parent if it doesn't have one. 4051 * 4052 * @return string 4053 */ 4054 public function get_collectionlabel() { 4055 if (!empty($this->collectionlabel)) { 4056 return $this->collectionlabel; 4057 } 4058 if ($this->parent && ($this->parent instanceof flat_navigation_node || $this->parent instanceof flat_navigation)) { 4059 return $this->parent->get_collectionlabel(); 4060 } 4061 debugging('Navigation region requires a label', DEBUG_DEVELOPER); 4062 return ''; 4063 } 4064 4065 /** 4066 * Does this node represent a course section link. 4067 * @return boolean 4068 */ 4069 public function is_section() { 4070 return $this->type == navigation_node::TYPE_SECTION; 4071 } 4072 4073 /** 4074 * In flat navigation - sections are active if we are looking at activities in the section. 4075 * @return boolean 4076 */ 4077 public function isactive() { 4078 global $PAGE; 4079 4080 if ($this->is_section()) { 4081 $active = $PAGE->navigation->find_active_node(); 4082 if ($active) { 4083 while ($active = $active->parent) { 4084 if ($active->key == $this->key && $active->type == $this->type) { 4085 return true; 4086 } 4087 } 4088 } 4089 } 4090 return $this->isactive; 4091 } 4092 4093 /** 4094 * Getter for "showdivider" 4095 * @return boolean 4096 */ 4097 public function showdivider() { 4098 return $this->showdivider; 4099 } 4100 4101 /** 4102 * Setter for "showdivider" 4103 * @param $val boolean 4104 * @param $label string Label for the group of nodes 4105 */ 4106 public function set_showdivider($val, $label = '') { 4107 $this->showdivider = $val; 4108 if ($this->showdivider && empty($label)) { 4109 debugging('Navigation region requires a label', DEBUG_DEVELOPER); 4110 } else { 4111 $this->set_collectionlabel($label); 4112 } 4113 } 4114 4115 /** 4116 * Getter for "indent" 4117 * @return boolean 4118 */ 4119 public function get_indent() { 4120 return $this->indent; 4121 } 4122 4123 /** 4124 * Setter for "indent" 4125 * @param $val boolean 4126 */ 4127 public function set_indent($val) { 4128 $this->indent = $val; 4129 } 4130 } 4131 4132 /** 4133 * Class used to generate a collection of navigation nodes most closely related 4134 * to the current page. 4135 * 4136 * @deprecated since Moodle 4.0 - do not use any more. Leverage secondary/tertiary navigation concepts 4137 * @package core 4138 * @copyright 2016 Damyon Wiese 4139 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 4140 */ 4141 class flat_navigation extends navigation_node_collection { 4142 /** @var moodle_page the moodle page that the navigation belongs to */ 4143 protected $page; 4144 4145 /** 4146 * Constructor. 4147 * 4148 * @param moodle_page $page 4149 */ 4150 public function __construct(moodle_page &$page) { 4151 if (during_initial_install()) { 4152 return false; 4153 } 4154 debugging("Flat navigation has been deprecated in favour of primary/secondary navigation concepts"); 4155 $this->page = $page; 4156 } 4157 4158 /** 4159 * Build the list of navigation nodes based on the current navigation and settings trees. 4160 * 4161 */ 4162 public function initialise() { 4163 global $PAGE, $USER, $OUTPUT, $CFG; 4164 if (during_initial_install()) { 4165 return; 4166 } 4167 4168 $current = false; 4169 4170 $course = $PAGE->course; 4171 4172 $this->page->navigation->initialise(); 4173 4174 // First walk the nav tree looking for "flat_navigation" nodes. 4175 if ($course->id > 1) { 4176 // It's a real course. 4177 $url = new moodle_url('/course/view.php', array('id' => $course->id)); 4178 4179 $coursecontext = context_course::instance($course->id, MUST_EXIST); 4180 $displaycontext = \context_helper::get_navigation_filter_context($coursecontext); 4181 // This is the name that will be shown for the course. 4182 $coursename = empty($CFG->navshowfullcoursenames) ? 4183 format_string($course->shortname, true, ['context' => $displaycontext]) : 4184 format_string($course->fullname, true, ['context' => $displaycontext]); 4185 4186 $flat = new flat_navigation_node(navigation_node::create($coursename, $url), 0); 4187 $flat->set_collectionlabel($coursename); 4188 $flat->key = 'coursehome'; 4189 $flat->icon = new pix_icon('i/course', ''); 4190 4191 $courseformat = course_get_format($course); 4192 $coursenode = $PAGE->navigation->find_active_node(); 4193 $targettype = navigation_node::TYPE_COURSE; 4194 4195 // Single activity format has no course node - the course node is swapped for the activity node. 4196 if (!$courseformat->has_view_page()) { 4197 $targettype = navigation_node::TYPE_ACTIVITY; 4198 } 4199 4200 while (!empty($coursenode) && ($coursenode->type != $targettype)) { 4201 $coursenode = $coursenode->parent; 4202 } 4203 // There is one very strange page in mod/feedback/view.php which thinks it is both site and course 4204 // context at the same time. That page is broken but we need to handle it (hence the SITEID). 4205 if ($coursenode && $coursenode->key != SITEID) { 4206 $this->add($flat); 4207 foreach ($coursenode->children as $child) { 4208 if ($child->action) { 4209 $flat = new flat_navigation_node($child, 0); 4210 $this->add($flat); 4211 } 4212 } 4213 } 4214 4215 $this->page->navigation->build_flat_navigation_list($this, true, get_string('site')); 4216 } else { 4217 $this->page->navigation->build_flat_navigation_list($this, false, get_string('site')); 4218 } 4219 4220 $admin = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN); 4221 if (!$admin) { 4222 // Try again - crazy nav tree! 4223 $admin = $PAGE->settingsnav->find('root', navigation_node::TYPE_SITE_ADMIN); 4224 } 4225 if ($admin) { 4226 $flat = new flat_navigation_node($admin, 0); 4227 $flat->set_showdivider(true, get_string('sitesettings')); 4228 $flat->key = 'sitesettings'; 4229 $flat->icon = new pix_icon('t/preferences', ''); 4230 $this->add($flat); 4231 } 4232 4233 // Add-a-block in editing mode. 4234 if (isset($this->page->theme->addblockposition) && 4235 $this->page->theme->addblockposition == BLOCK_ADDBLOCK_POSITION_FLATNAV && 4236 $PAGE->user_is_editing() && $PAGE->user_can_edit_blocks()) { 4237 $url = new moodle_url($PAGE->url, ['bui_addblock' => '', 'sesskey' => sesskey()]); 4238 $addablock = navigation_node::create(get_string('addblock'), $url); 4239 $flat = new flat_navigation_node($addablock, 0); 4240 $flat->set_showdivider(true, get_string('blocksaddedit')); 4241 $flat->key = 'addblock'; 4242 $flat->icon = new pix_icon('i/addblock', ''); 4243 $this->add($flat); 4244 4245 $addblockurl = "?{$url->get_query_string(false)}"; 4246 4247 $PAGE->requires->js_call_amd('core_block/add_modal', 'init', 4248 [$addblockurl, $this->page->get_edited_page_hash()]); 4249 } 4250 } 4251 4252 /** 4253 * Override the parent so we can set a label for this collection if it has not been set yet. 4254 * 4255 * @param navigation_node $node Node to add 4256 * @param string $beforekey If specified, adds before a node with this key, 4257 * otherwise adds at end 4258 * @return navigation_node Added node 4259 */ 4260 public function add(navigation_node $node, $beforekey=null) { 4261 $result = parent::add($node, $beforekey); 4262 // Extend the parent to get a name for the collection of nodes if required. 4263 if (empty($this->collectionlabel)) { 4264 if ($node instanceof flat_navigation_node) { 4265 $this->set_collectionlabel($node->get_collectionlabel()); 4266 } 4267 } 4268 4269 return $result; 4270 } 4271 } 4272 4273 /** 4274 * Class used to manage the settings option for the current page 4275 * 4276 * This class is used to manage the settings options in a tree format (recursively) 4277 * and was created initially for use with the settings blocks. 4278 * 4279 * @package core 4280 * @category navigation 4281 * @copyright 2009 Sam Hemelryk 4282 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 4283 */ 4284 class settings_navigation extends navigation_node { 4285 /** @var context the current context */ 4286 protected $context; 4287 /** @var moodle_page the moodle page that the navigation belongs to */ 4288 protected $page; 4289 /** @var string contains administration section navigation_nodes */ 4290 protected $adminsection; 4291 /** @var bool A switch to see if the navigation node is initialised */ 4292 protected $initialised = false; 4293 /** @var array An array of users that the nodes can extend for. */ 4294 protected $userstoextendfor = array(); 4295 /** @var navigation_cache **/ 4296 protected $cache; 4297 4298 /** 4299 * Sets up the object with basic settings and preparse it for use 4300 * 4301 * @param moodle_page $page 4302 */ 4303 public function __construct(moodle_page &$page) { 4304 if (during_initial_install()) { 4305 return false; 4306 } 4307 $this->page = $page; 4308 // Initialise the main navigation. It is most important that this is done 4309 // before we try anything 4310 $this->page->navigation->initialise(); 4311 // Initialise the navigation cache 4312 $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME); 4313 $this->children = new navigation_node_collection(); 4314 } 4315 4316 /** 4317 * Initialise the settings navigation based on the current context 4318 * 4319 * This function initialises the settings navigation tree for a given context 4320 * by calling supporting functions to generate major parts of the tree. 4321 * 4322 */ 4323 public function initialise() { 4324 global $DB, $SESSION, $SITE; 4325 4326 if (during_initial_install()) { 4327 return false; 4328 } else if ($this->initialised) { 4329 return true; 4330 } 4331 $this->id = 'settingsnav'; 4332 $this->context = $this->page->context; 4333 4334 $context = $this->context; 4335 if ($context->contextlevel == CONTEXT_BLOCK) { 4336 $this->load_block_settings(); 4337 $context = $context->get_parent_context(); 4338 $this->context = $context; 4339 } 4340 switch ($context->contextlevel) { 4341 case CONTEXT_SYSTEM: 4342 if ($this->page->url->compare(new moodle_url('/admin/settings.php', array('section'=>'frontpagesettings')))) { 4343 $this->load_front_page_settings(($context->id == $this->context->id)); 4344 } 4345 break; 4346 case CONTEXT_COURSECAT: 4347 $this->load_category_settings(); 4348 break; 4349 case CONTEXT_COURSE: 4350 if ($this->page->course->id != $SITE->id) { 4351 $this->load_course_settings(($context->id == $this->context->id)); 4352 } else { 4353 $this->load_front_page_settings(($context->id == $this->context->id)); 4354 } 4355 break; 4356 case CONTEXT_MODULE: 4357 $this->load_module_settings(); 4358 $this->load_course_settings(); 4359 break; 4360 case CONTEXT_USER: 4361 if ($this->page->course->id != $SITE->id) { 4362 $this->load_course_settings(); 4363 } 4364 break; 4365 } 4366 4367 $usersettings = $this->load_user_settings($this->page->course->id); 4368 4369 $adminsettings = false; 4370 if (isloggedin() && !isguestuser() && (!isset($SESSION->load_navigation_admin) || $SESSION->load_navigation_admin)) { 4371 $isadminpage = $this->is_admin_tree_needed(); 4372 4373 if (has_capability('moodle/site:configview', context_system::instance())) { 4374 if (has_capability('moodle/site:config', context_system::instance())) { 4375 // Make sure this works even if config capability changes on the fly 4376 // and also make it fast for admin right after login. 4377 $SESSION->load_navigation_admin = 1; 4378 if ($isadminpage) { 4379 $adminsettings = $this->load_administration_settings(); 4380 } 4381 4382 } else if (!isset($SESSION->load_navigation_admin)) { 4383 $adminsettings = $this->load_administration_settings(); 4384 $SESSION->load_navigation_admin = (int)($adminsettings->children->count() > 0); 4385 4386 } else if ($SESSION->load_navigation_admin) { 4387 if ($isadminpage) { 4388 $adminsettings = $this->load_administration_settings(); 4389 } 4390 } 4391 4392 // Print empty navigation node, if needed. 4393 if ($SESSION->load_navigation_admin && !$isadminpage) { 4394 if ($adminsettings) { 4395 // Do not print settings tree on pages that do not need it, this helps with performance. 4396 $adminsettings->remove(); 4397 $adminsettings = false; 4398 } 4399 $siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin/search.php'), 4400 self::TYPE_SITE_ADMIN, null, 'siteadministration'); 4401 $siteadminnode->id = 'expandable_branch_' . $siteadminnode->type . '_' . 4402 clean_param($siteadminnode->key, PARAM_ALPHANUMEXT); 4403 $siteadminnode->requiresajaxloading = 'true'; 4404 } 4405 } 4406 } 4407 4408 if ($context->contextlevel == CONTEXT_SYSTEM && $adminsettings) { 4409 $adminsettings->force_open(); 4410 } else if ($context->contextlevel == CONTEXT_USER && $usersettings) { 4411 $usersettings->force_open(); 4412 } 4413 4414 // At this point we give any local plugins the ability to extend/tinker with the navigation settings. 4415 $this->load_local_plugin_settings(); 4416 4417 foreach ($this->children as $key=>$node) { 4418 if ($node->nodetype == self::NODETYPE_BRANCH && $node->children->count() == 0) { 4419 // Site administration is shown as link. 4420 if (!empty($SESSION->load_navigation_admin) && ($node->type === self::TYPE_SITE_ADMIN)) { 4421 continue; 4422 } 4423 $node->remove(); 4424 } 4425 } 4426 $this->initialised = true; 4427 } 4428 /** 4429 * Override the parent function so that we can add preceeding hr's and set a 4430 * root node class against all first level element 4431 * 4432 * It does this by first calling the parent's add method {@link navigation_node::add()} 4433 * and then proceeds to use the key to set class and hr 4434 * 4435 * @param string $text text to be used for the link. 4436 * @param string|moodle_url $url url for the new node 4437 * @param int $type the type of node navigation_node::TYPE_* 4438 * @param string $shorttext 4439 * @param string|int $key a key to access the node by. 4440 * @param pix_icon $icon An icon that appears next to the node. 4441 * @return navigation_node with the new node added to it. 4442 */ 4443 public function add($text, $url=null, $type=null, $shorttext=null, $key=null, pix_icon $icon=null) { 4444 $node = parent::add($text, $url, $type, $shorttext, $key, $icon); 4445 $node->add_class('root_node'); 4446 return $node; 4447 } 4448 4449 /** 4450 * This function allows the user to add something to the start of the settings 4451 * navigation, which means it will be at the top of the settings navigation block 4452 * 4453 * @param string $text text to be used for the link. 4454 * @param string|moodle_url $url url for the new node 4455 * @param int $type the type of node navigation_node::TYPE_* 4456 * @param string $shorttext 4457 * @param string|int $key a key to access the node by. 4458 * @param pix_icon $icon An icon that appears next to the node. 4459 * @return navigation_node $node with the new node added to it. 4460 */ 4461 public function prepend($text, $url=null, $type=null, $shorttext=null, $key=null, pix_icon $icon=null) { 4462 $children = $this->children; 4463 $childrenclass = get_class($children); 4464 $this->children = new $childrenclass; 4465 $node = $this->add($text, $url, $type, $shorttext, $key, $icon); 4466 foreach ($children as $child) { 4467 $this->children->add($child); 4468 } 4469 return $node; 4470 } 4471 4472 /** 4473 * Does this page require loading of full admin tree or is 4474 * it enough rely on AJAX? 4475 * 4476 * @return bool 4477 */ 4478 protected function is_admin_tree_needed() { 4479 if (self::$loadadmintree) { 4480 // Usually external admin page or settings page. 4481 return true; 4482 } 4483 4484 if ($this->page->pagelayout === 'admin' or strpos($this->page->pagetype, 'admin-') === 0) { 4485 // Admin settings tree is intended for system level settings and management only, use navigation for the rest! 4486 if ($this->page->context->contextlevel != CONTEXT_SYSTEM) { 4487 return false; 4488 } 4489 return true; 4490 } 4491 4492 return false; 4493 } 4494 4495 /** 4496 * Load the site administration tree 4497 * 4498 * This function loads the site administration tree by using the lib/adminlib library functions 4499 * 4500 * @param navigation_node $referencebranch A reference to a branch in the settings 4501 * navigation tree 4502 * @param part_of_admin_tree $adminbranch The branch to add, if null generate the admin 4503 * tree and start at the beginning 4504 * @return mixed A key to access the admin tree by 4505 */ 4506 protected function load_administration_settings(navigation_node $referencebranch=null, part_of_admin_tree $adminbranch=null) { 4507 global $CFG; 4508 4509 // Check if we are just starting to generate this navigation. 4510 if ($referencebranch === null) { 4511 4512 // Require the admin lib then get an admin structure 4513 if (!function_exists('admin_get_root')) { 4514 require_once($CFG->dirroot.'/lib/adminlib.php'); 4515 } 4516 $adminroot = admin_get_root(false, false); 4517 // This is the active section identifier 4518 $this->adminsection = $this->page->url->param('section'); 4519 4520 // Disable the navigation from automatically finding the active node 4521 navigation_node::$autofindactive = false; 4522 $referencebranch = $this->add(get_string('administrationsite'), '/admin/search.php', self::TYPE_SITE_ADMIN, null, 'root'); 4523 foreach ($adminroot->children as $adminbranch) { 4524 $this->load_administration_settings($referencebranch, $adminbranch); 4525 } 4526 navigation_node::$autofindactive = true; 4527 4528 // Use the admin structure to locate the active page 4529 if (!$this->contains_active_node() && $current = $adminroot->locate($this->adminsection, true)) { 4530 $currentnode = $this; 4531 while (($pathkey = array_pop($current->path))!==null && $currentnode) { 4532 $currentnode = $currentnode->get($pathkey); 4533 } 4534 if ($currentnode) { 4535 $currentnode->make_active(); 4536 } 4537 } else { 4538 $this->scan_for_active_node($referencebranch); 4539 } 4540 return $referencebranch; 4541 } else if ($adminbranch->check_access()) { 4542 // We have a reference branch that we can access and is not hidden `hurrah` 4543 // Now we need to display it and any children it may have 4544 $url = null; 4545 $icon = null; 4546 4547 if ($adminbranch instanceof \core_admin\local\settings\linkable_settings_page) { 4548 if (empty($CFG->linkadmincategories) && $adminbranch instanceof admin_category) { 4549 $url = null; 4550 } else { 4551 $url = $adminbranch->get_settings_page_url(); 4552 } 4553 } 4554 4555 // Add the branch 4556 $reference = $referencebranch->add($adminbranch->visiblename, $url, self::TYPE_SETTING, null, $adminbranch->name, $icon); 4557 4558 if ($adminbranch->is_hidden()) { 4559 if (($adminbranch instanceof admin_externalpage || $adminbranch instanceof admin_settingpage) && $adminbranch->name == $this->adminsection) { 4560 $reference->add_class('hidden'); 4561 } else { 4562 $reference->display = false; 4563 } 4564 } 4565 4566 // Check if we are generating the admin notifications and whether notificiations exist 4567 if ($adminbranch->name === 'adminnotifications' && admin_critical_warnings_present()) { 4568 $reference->add_class('criticalnotification'); 4569 } 4570 // Check if this branch has children 4571 if ($reference && isset($adminbranch->children) && is_array($adminbranch->children) && count($adminbranch->children)>0) { 4572 foreach ($adminbranch->children as $branch) { 4573 // Generate the child branches as well now using this branch as the reference 4574 $this->load_administration_settings($reference, $branch); 4575 } 4576 } else { 4577 $reference->icon = new pix_icon('i/settings', ''); 4578 } 4579 } 4580 } 4581 4582 /** 4583 * This function recursivily scans nodes until it finds the active node or there 4584 * are no more nodes. 4585 * @param navigation_node $node 4586 */ 4587 protected function scan_for_active_node(navigation_node $node) { 4588 if (!$node->check_if_active() && $node->children->count()>0) { 4589 foreach ($node->children as &$child) { 4590 $this->scan_for_active_node($child); 4591 } 4592 } 4593 } 4594 4595 /** 4596 * Gets a navigation node given an array of keys that represent the path to 4597 * the desired node. 4598 * 4599 * @param array $path 4600 * @return navigation_node|false 4601 */ 4602 protected function get_by_path(array $path) { 4603 $node = $this->get(array_shift($path)); 4604 foreach ($path as $key) { 4605 $node->get($key); 4606 } 4607 return $node; 4608 } 4609 4610 /** 4611 * This function loads the course settings that are available for the user 4612 * 4613 * @param bool $forceopen If set to true the course node will be forced open 4614 * @return navigation_node|false 4615 */ 4616 protected function load_course_settings($forceopen = false) { 4617 global $CFG, $USER; 4618 require_once($CFG->dirroot . '/course/lib.php'); 4619 4620 $course = $this->page->course; 4621 $coursecontext = context_course::instance($course->id); 4622 $adminoptions = course_get_user_administration_options($course, $coursecontext); 4623 4624 // note: do not test if enrolled or viewing here because we need the enrol link in Course administration section 4625 4626 $coursenode = $this->add(get_string('courseadministration'), null, self::TYPE_COURSE, null, 'courseadmin'); 4627 if ($forceopen) { 4628 $coursenode->force_open(); 4629 } 4630 4631 4632 if ($adminoptions->update) { 4633 // Add the course settings link 4634 $url = new moodle_url('/course/edit.php', array('id'=>$course->id)); 4635 $coursenode->add(get_string('settings'), $url, self::TYPE_SETTING, null, 4636 'editsettings', new pix_icon('i/settings', '')); 4637 } 4638 4639 if ($adminoptions->editcompletion) { 4640 // Add the course completion settings link 4641 $url = new moodle_url('/course/completion.php', array('id' => $course->id)); 4642 $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, 'coursecompletion', 4643 new pix_icon('i/settings', '')); 4644 } 4645 4646 if (!$adminoptions->update && $adminoptions->tags) { 4647 $url = new moodle_url('/course/tags.php', array('id' => $course->id)); 4648 $coursenode->add(get_string('coursetags', 'tag'), $url, self::TYPE_SETTING, null, 'coursetags', new pix_icon('i/settings', '')); 4649 $coursenode->get('coursetags')->set_force_into_more_menu(); 4650 } 4651 4652 // add enrol nodes 4653 enrol_add_course_navigation($coursenode, $course); 4654 4655 // Manage filters 4656 if ($adminoptions->filters) { 4657 $url = new moodle_url('/filter/manage.php', array('contextid'=>$coursecontext->id)); 4658 $coursenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, 4659 null, 'filtermanagement', new pix_icon('i/filter', '')); 4660 } 4661 4662 // View course reports. 4663 if ($adminoptions->reports) { 4664 $reportnav = $coursenode->add(get_string('reports'), 4665 new moodle_url('/report/view.php', ['courseid' => $coursecontext->instanceid]), 4666 self::TYPE_CONTAINER, null, 'coursereports', new pix_icon('i/stats', '')); 4667 $coursereports = core_component::get_plugin_list('coursereport'); 4668 foreach ($coursereports as $report => $dir) { 4669 $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php'; 4670 if (file_exists($libfile)) { 4671 require_once($libfile); 4672 $reportfunction = $report.'_report_extend_navigation'; 4673 if (function_exists($report.'_report_extend_navigation')) { 4674 $reportfunction($reportnav, $course, $coursecontext); 4675 } 4676 } 4677 } 4678 4679 $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php'); 4680 foreach ($reports as $reportfunction) { 4681 $reportfunction($reportnav, $course, $coursecontext); 4682 } 4683 4684 if (!$reportnav->has_children()) { 4685 $reportnav->remove(); 4686 } 4687 } 4688 4689 // Check if we can view the gradebook's setup page. 4690 if ($adminoptions->gradebook) { 4691 $url = new moodle_url('/grade/edit/tree/index.php', array('id' => $course->id)); 4692 $coursenode->add(get_string('gradebooksetup', 'grades'), $url, self::TYPE_SETTING, 4693 null, 'gradebooksetup', new pix_icon('i/settings', '')); 4694 } 4695 4696 // Add the context locking node. 4697 $this->add_context_locking_node($coursenode, $coursecontext); 4698 4699 // Add outcome if permitted 4700 if ($adminoptions->outcomes) { 4701 $url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$course->id)); 4702 $coursenode->add(get_string('outcomes', 'grades'), $url, self::TYPE_SETTING, null, 'outcomes', new pix_icon('i/outcomes', '')); 4703 } 4704 4705 //Add badges navigation 4706 if ($adminoptions->badges) { 4707 require_once($CFG->libdir .'/badgeslib.php'); 4708 badges_add_course_navigation($coursenode, $course); 4709 } 4710 4711 // Import data from other courses. 4712 if ($adminoptions->import) { 4713 $url = new moodle_url('/backup/import.php', array('id' => $course->id)); 4714 $coursenode->add(get_string('import'), $url, self::TYPE_SETTING, null, 'import', new pix_icon('i/import', '')); 4715 } 4716 4717 // Backup this course 4718 if ($adminoptions->backup) { 4719 $url = new moodle_url('/backup/backup.php', array('id'=>$course->id)); 4720 $coursenode->add(get_string('backup'), $url, self::TYPE_SETTING, null, 'backup', new pix_icon('i/backup', '')); 4721 } 4722 4723 // Restore to this course 4724 if ($adminoptions->restore) { 4725 $url = new moodle_url('/backup/restorefile.php', array('contextid'=>$coursecontext->id)); 4726 $coursenode->add(get_string('restore'), $url, self::TYPE_SETTING, null, 'restore', new pix_icon('i/restore', '')); 4727 } 4728 4729 // Copy this course. 4730 if ($adminoptions->copy) { 4731 $url = new moodle_url('/backup/copy.php', array('id' => $course->id)); 4732 $coursenode->add(get_string('copycourse'), $url, self::TYPE_SETTING, null, 'copy', new pix_icon('t/copy', '')); 4733 } 4734 4735 // Reset this course 4736 if ($adminoptions->reset) { 4737 $url = new moodle_url('/course/reset.php', array('id'=>$course->id)); 4738 $coursenode->add(get_string('reset'), $url, self::TYPE_SETTING, null, 'reset', new pix_icon('i/return', '')); 4739 } 4740 4741 // Questions 4742 require_once($CFG->libdir . '/questionlib.php'); 4743 question_extend_settings_navigation($coursenode, $coursecontext)->trim_if_empty(); 4744 4745 if ($adminoptions->update) { 4746 // Repository Instances 4747 if (!$this->cache->cached('contexthasrepos'.$coursecontext->id)) { 4748 require_once($CFG->dirroot . '/repository/lib.php'); 4749 $editabletypes = repository::get_editable_types($coursecontext); 4750 $haseditabletypes = !empty($editabletypes); 4751 unset($editabletypes); 4752 $this->cache->set('contexthasrepos'.$coursecontext->id, $haseditabletypes); 4753 } else { 4754 $haseditabletypes = $this->cache->{'contexthasrepos'.$coursecontext->id}; 4755 } 4756 if ($haseditabletypes) { 4757 $url = new moodle_url('/repository/manage_instances.php', array('contextid' => $coursecontext->id)); 4758 $coursenode->add(get_string('repositories'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/repository', '')); 4759 } 4760 } 4761 4762 // Manage files 4763 if ($adminoptions->files) { 4764 // hidden in new courses and courses where legacy files were turned off 4765 $url = new moodle_url('/files/index.php', array('contextid'=>$coursecontext->id)); 4766 $coursenode->add(get_string('courselegacyfiles'), $url, self::TYPE_SETTING, null, 'coursefiles', new pix_icon('i/folder', '')); 4767 4768 } 4769 4770 // Let plugins hook into course navigation. 4771 $pluginsfunction = get_plugins_with_function('extend_navigation_course', 'lib.php'); 4772 foreach ($pluginsfunction as $plugintype => $plugins) { 4773 // Ignore the report plugin as it was already loaded above. 4774 if ($plugintype == 'report') { 4775 continue; 4776 } 4777 foreach ($plugins as $pluginfunction) { 4778 $pluginfunction($coursenode, $course, $coursecontext); 4779 } 4780 } 4781 4782 // Prepare data for course content download functionality if it is enabled. 4783 if (\core\content::can_export_context($coursecontext, $USER)) { 4784 $linkattr = \core_course\output\content_export_link::get_attributes($coursecontext); 4785 $actionlink = new action_link($linkattr->url, $linkattr->displaystring, null, $linkattr->elementattributes); 4786 4787 $coursenode->add($linkattr->displaystring, $actionlink, self::TYPE_SETTING, null, 'download', 4788 new pix_icon('t/download', '')); 4789 $coursenode->get('download')->set_force_into_more_menu(true); 4790 } 4791 4792 // Return we are done 4793 return $coursenode; 4794 } 4795 4796 /** 4797 * Get the moodle_page object associated to the current settings navigation. 4798 * 4799 * @return moodle_page 4800 */ 4801 public function get_page(): moodle_page { 4802 return $this->page; 4803 } 4804 4805 /** 4806 * This function calls the module function to inject module settings into the 4807 * settings navigation tree. 4808 * 4809 * This only gets called if there is a corrosponding function in the modules 4810 * lib file. 4811 * 4812 * For examples mod/forum/lib.php {@link forum_extend_settings_navigation()} 4813 * 4814 * @return navigation_node|false 4815 */ 4816 protected function load_module_settings() { 4817 global $CFG, $USER; 4818 4819 if (!$this->page->cm && $this->context->contextlevel == CONTEXT_MODULE && $this->context->instanceid) { 4820 $cm = get_coursemodule_from_id(false, $this->context->instanceid, 0, false, MUST_EXIST); 4821 $this->page->set_cm($cm, $this->page->course); 4822 } 4823 4824 $file = $CFG->dirroot.'/mod/'.$this->page->activityname.'/lib.php'; 4825 if (file_exists($file)) { 4826 require_once($file); 4827 } 4828 4829 $modulenode = $this->add(get_string('pluginadministration', $this->page->activityname), null, self::TYPE_SETTING, null, 'modulesettings'); 4830 $modulenode->nodetype = navigation_node::NODETYPE_BRANCH; 4831 $modulenode->force_open(); 4832 4833 // Settings for the module 4834 if (has_capability('moodle/course:manageactivities', $this->page->cm->context)) { 4835 $url = new moodle_url('/course/modedit.php', array('update' => $this->page->cm->id, 'return' => 1)); 4836 $modulenode->add(get_string('settings'), $url, self::TYPE_SETTING, null, 'modedit', new pix_icon('i/settings', '')); 4837 } 4838 // Assign local roles 4839 if (count(get_assignable_roles($this->page->cm->context))>0) { 4840 $url = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid'=>$this->page->cm->context->id)); 4841 $modulenode->add(get_string('localroles', 'role'), $url, self::TYPE_SETTING, null, 'roleassign', 4842 new pix_icon('i/role', '')); 4843 } 4844 // Override roles 4845 if (has_capability('moodle/role:review', $this->page->cm->context) or count(get_overridable_roles($this->page->cm->context))>0) { 4846 $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid'=>$this->page->cm->context->id)); 4847 $modulenode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null, 'roleoverride', 4848 new pix_icon('i/permissions', '')); 4849 } 4850 // Check role permissions 4851 if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $this->page->cm->context)) { 4852 $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->page->cm->context->id)); 4853 $modulenode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'rolecheck', 4854 new pix_icon('i/checkpermissions', '')); 4855 } 4856 4857 // Add the context locking node. 4858 $this->add_context_locking_node($modulenode, $this->page->cm->context); 4859 4860 // Manage filters 4861 if (has_capability('moodle/filter:manage', $this->page->cm->context) && count(filter_get_available_in_context($this->page->cm->context))>0) { 4862 $url = new moodle_url('/filter/manage.php', array('contextid'=>$this->page->cm->context->id)); 4863 $modulenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, 'filtermanage', 4864 new pix_icon('i/filter', '')); 4865 } 4866 // Add reports 4867 $reports = get_plugin_list_with_function('report', 'extend_navigation_module', 'lib.php'); 4868 foreach ($reports as $reportfunction) { 4869 $reportfunction($modulenode, $this->page->cm); 4870 } 4871 // Add a backup link 4872 $featuresfunc = $this->page->activityname.'_supports'; 4873 if (function_exists($featuresfunc) && $featuresfunc(FEATURE_BACKUP_MOODLE2) && has_capability('moodle/backup:backupactivity', $this->page->cm->context)) { 4874 $url = new moodle_url('/backup/backup.php', array('id'=>$this->page->cm->course, 'cm'=>$this->page->cm->id)); 4875 $modulenode->add(get_string('backup'), $url, self::TYPE_SETTING, null, 'backup', new pix_icon('i/backup', '')); 4876 } 4877 4878 // Restore this activity 4879 $featuresfunc = $this->page->activityname.'_supports'; 4880 if (function_exists($featuresfunc) && $featuresfunc(FEATURE_BACKUP_MOODLE2) && has_capability('moodle/restore:restoreactivity', $this->page->cm->context)) { 4881 $url = new moodle_url('/backup/restorefile.php', array('contextid'=>$this->page->cm->context->id)); 4882 $modulenode->add(get_string('restore'), $url, self::TYPE_SETTING, null, 'restore', new pix_icon('i/restore', '')); 4883 } 4884 4885 // Allow the active advanced grading method plugin to append its settings 4886 $featuresfunc = $this->page->activityname.'_supports'; 4887 if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING) && has_capability('moodle/grade:managegradingforms', $this->page->cm->context)) { 4888 require_once($CFG->dirroot.'/grade/grading/lib.php'); 4889 $gradingman = get_grading_manager($this->page->cm->context, 'mod_'.$this->page->activityname); 4890 $gradingman->extend_settings_navigation($this, $modulenode); 4891 } 4892 4893 $function = $this->page->activityname.'_extend_settings_navigation'; 4894 if (function_exists($function)) { 4895 $function($this, $modulenode); 4896 } 4897 4898 // Send to MoodleNet. 4899 $usercanshare = utilities::can_user_share($this->context->get_course_context(), $USER->id); 4900 $issuerid = get_config('moodlenet', 'oauthservice'); 4901 try { 4902 $issuer = \core\oauth2\api::get_issuer($issuerid); 4903 $isvalidinstance = utilities::is_valid_instance($issuer); 4904 if ($usercanshare && $isvalidinstance) { 4905 $this->page->requires->js_call_amd('core/moodlenet/send_resource', 'init'); 4906 $action = new action_link(new moodle_url(''), '', null, [ 4907 'data-action' => 'sendtomoodlenet', 4908 'data-type' => 'activity', 4909 'data-sharetype' => 'resource', 4910 ]); 4911 $modulenode->add(get_string('moodlenet:sharetomoodlenet', 'moodle'), 4912 $action, self::TYPE_SETTING, null, 'exportmoodlenet')->set_force_into_more_menu(true); 4913 } 4914 } catch (dml_missing_record_exception $e) { 4915 debugging("Invalid MoodleNet OAuth 2 service set in site administration: 'moodlenet | oauthservice'. " . 4916 "This must be a valid issuer."); 4917 } 4918 4919 // Remove the module node if there are no children. 4920 if ($modulenode->children->count() <= 0) { 4921 $modulenode->remove(); 4922 } 4923 4924 return $modulenode; 4925 } 4926 4927 /** 4928 * Loads the user settings block of the settings nav 4929 * 4930 * This function is simply works out the userid and whether we need to load 4931 * just the current users profile settings, or the current user and the user the 4932 * current user is viewing. 4933 * 4934 * This function has some very ugly code to work out the user, if anyone has 4935 * any bright ideas please feel free to intervene. 4936 * 4937 * @param int $courseid The course id of the current course 4938 * @return navigation_node|false 4939 */ 4940 protected function load_user_settings($courseid = SITEID) { 4941 global $USER, $CFG; 4942 4943 if (isguestuser() || !isloggedin()) { 4944 return false; 4945 } 4946 4947 $navusers = $this->page->navigation->get_extending_users(); 4948 4949 if (count($this->userstoextendfor) > 0 || count($navusers) > 0) { 4950 $usernode = null; 4951 foreach ($this->userstoextendfor as $userid) { 4952 if ($userid == $USER->id) { 4953 continue; 4954 } 4955 $node = $this->generate_user_settings($courseid, $userid, 'userviewingsettings'); 4956 if (is_null($usernode)) { 4957 $usernode = $node; 4958 } 4959 } 4960 foreach ($navusers as $user) { 4961 if ($user->id == $USER->id) { 4962 continue; 4963 } 4964 $node = $this->generate_user_settings($courseid, $user->id, 'userviewingsettings'); 4965 if (is_null($usernode)) { 4966 $usernode = $node; 4967 } 4968 } 4969 $this->generate_user_settings($courseid, $USER->id); 4970 } else { 4971 $usernode = $this->generate_user_settings($courseid, $USER->id); 4972 } 4973 return $usernode; 4974 } 4975 4976 /** 4977 * Extends the settings navigation for the given user. 4978 * 4979 * Note: This method gets called automatically if you call 4980 * $PAGE->navigation->extend_for_user($userid) 4981 * 4982 * @param int $userid 4983 */ 4984 public function extend_for_user($userid) { 4985 global $CFG; 4986 4987 if (!in_array($userid, $this->userstoextendfor)) { 4988 $this->userstoextendfor[] = $userid; 4989 if ($this->initialised) { 4990 $this->generate_user_settings($this->page->course->id, $userid, 'userviewingsettings'); 4991 $children = array(); 4992 foreach ($this->children as $child) { 4993 $children[] = $child; 4994 } 4995 array_unshift($children, array_pop($children)); 4996 $this->children = new navigation_node_collection(); 4997 foreach ($children as $child) { 4998 $this->children->add($child); 4999 } 5000 } 5001 } 5002 } 5003 5004 /** 5005 * This function gets called by {@link settings_navigation::load_user_settings()} and actually works out 5006 * what can be shown/done 5007 * 5008 * @param int $courseid The current course' id 5009 * @param int $userid The user id to load for 5010 * @param string $gstitle The string to pass to get_string for the branch title 5011 * @return navigation_node|false 5012 */ 5013 protected function generate_user_settings($courseid, $userid, $gstitle='usercurrentsettings') { 5014 global $DB, $CFG, $USER, $SITE; 5015 5016 if ($courseid != $SITE->id) { 5017 if (!empty($this->page->course->id) && $this->page->course->id == $courseid) { 5018 $course = $this->page->course; 5019 } else { 5020 $select = context_helper::get_preload_record_columns_sql('ctx'); 5021 $sql = "SELECT c.*, $select 5022 FROM {course} c 5023 JOIN {context} ctx ON c.id = ctx.instanceid 5024 WHERE c.id = :courseid AND ctx.contextlevel = :contextlevel"; 5025 $params = array('courseid' => $courseid, 'contextlevel' => CONTEXT_COURSE); 5026 $course = $DB->get_record_sql($sql, $params, MUST_EXIST); 5027 context_helper::preload_from_record($course); 5028 } 5029 } else { 5030 $course = $SITE; 5031 } 5032 5033 $coursecontext = context_course::instance($course->id); // Course context 5034 $systemcontext = context_system::instance(); 5035 $currentuser = ($USER->id == $userid); 5036 5037 if ($currentuser) { 5038 $user = $USER; 5039 $usercontext = context_user::instance($user->id); // User context 5040 } else { 5041 $select = context_helper::get_preload_record_columns_sql('ctx'); 5042 $sql = "SELECT u.*, $select 5043 FROM {user} u 5044 JOIN {context} ctx ON u.id = ctx.instanceid 5045 WHERE u.id = :userid AND ctx.contextlevel = :contextlevel"; 5046 $params = array('userid' => $userid, 'contextlevel' => CONTEXT_USER); 5047 $user = $DB->get_record_sql($sql, $params, IGNORE_MISSING); 5048 if (!$user) { 5049 return false; 5050 } 5051 context_helper::preload_from_record($user); 5052 5053 // Check that the user can view the profile 5054 $usercontext = context_user::instance($user->id); // User context 5055 $canviewuser = has_capability('moodle/user:viewdetails', $usercontext); 5056 5057 if ($course->id == $SITE->id) { 5058 if ($CFG->forceloginforprofiles && !has_coursecontact_role($user->id) && !$canviewuser) { // Reduce possibility of "browsing" userbase at site level 5059 // Teachers can browse and be browsed at site level. If not forceloginforprofiles, allow access (bug #4366) 5060 return false; 5061 } 5062 } else { 5063 $canviewusercourse = has_capability('moodle/user:viewdetails', $coursecontext); 5064 $userisenrolled = is_enrolled($coursecontext, $user->id, '', true); 5065 if ((!$canviewusercourse && !$canviewuser) || !$userisenrolled) { 5066 return false; 5067 } 5068 $canaccessallgroups = has_capability('moodle/site:accessallgroups', $coursecontext); 5069 if (!$canaccessallgroups && groups_get_course_groupmode($course) == SEPARATEGROUPS && !$canviewuser) { 5070 // If groups are in use, make sure we can see that group (MDL-45874). That does not apply to parents. 5071 if ($courseid == $this->page->course->id) { 5072 $mygroups = get_fast_modinfo($this->page->course)->groups; 5073 } else { 5074 $mygroups = groups_get_user_groups($courseid); 5075 } 5076 $usergroups = groups_get_user_groups($courseid, $userid); 5077 if (!array_intersect_key($mygroups[0], $usergroups[0])) { 5078 return false; 5079 } 5080 } 5081 } 5082 } 5083 5084 $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $this->page->context)); 5085 5086 $key = $gstitle; 5087 $prefurl = new moodle_url('/user/preferences.php'); 5088 if ($gstitle != 'usercurrentsettings') { 5089 $key .= $userid; 5090 $prefurl->param('userid', $userid); 5091 } 5092 5093 // Add a user setting branch. 5094 if ($gstitle == 'usercurrentsettings') { 5095 $mainpage = $this->add(get_string('home'), new moodle_url('/'), self::TYPE_CONTAINER, null, 'site'); 5096 5097 // This should be set to false as we don't want to show this to the user. It's only for generating the correct 5098 // breadcrumb. 5099 $mainpage->display = false; 5100 $homepage = get_home_page(); 5101 if (($homepage == HOMEPAGE_MY || $homepage == HOMEPAGE_MYCOURSES)) { 5102 $mainpage->mainnavonly = true; 5103 } 5104 5105 $iscurrentuser = ($user->id == $USER->id); 5106 5107 $baseargs = array('id' => $user->id); 5108 if ($course->id != $SITE->id && !$iscurrentuser) { 5109 $baseargs['course'] = $course->id; 5110 $issitecourse = false; 5111 } else { 5112 // Load all categories and get the context for the system. 5113 $issitecourse = true; 5114 } 5115 5116 // Add the user profile to the dashboard. 5117 $profilenode = $mainpage->add(get_string('profile'), new moodle_url('/user/profile.php', 5118 array('id' => $user->id)), self::TYPE_SETTING, null, 'myprofile'); 5119 5120 if (!empty($CFG->navadduserpostslinks)) { 5121 // Add nodes for forum posts and discussions if the user can view either or both 5122 // There are no capability checks here as the content of the page is based 5123 // purely on the forums the current user has access too. 5124 $forumtab = $profilenode->add(get_string('forumposts', 'forum')); 5125 $forumtab->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', $baseargs), null, 'myposts'); 5126 $forumtab->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', 5127 array_merge($baseargs, array('mode' => 'discussions'))), null, 'mydiscussions'); 5128 } 5129 5130 // Add blog nodes. 5131 if (!empty($CFG->enableblogs)) { 5132 if (!$this->cache->cached('userblogoptions'.$user->id)) { 5133 require_once($CFG->dirroot.'/blog/lib.php'); 5134 // Get all options for the user. 5135 $options = blog_get_options_for_user($user); 5136 $this->cache->set('userblogoptions'.$user->id, $options); 5137 } else { 5138 $options = $this->cache->{'userblogoptions'.$user->id}; 5139 } 5140 5141 if (count($options) > 0) { 5142 $blogs = $profilenode->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER); 5143 foreach ($options as $type => $option) { 5144 if ($type == "rss") { 5145 $blogs->add($option['string'], $option['link'], self::TYPE_SETTING, null, null, 5146 new pix_icon('i/rss', '')); 5147 } else { 5148 $blogs->add($option['string'], $option['link'], self::TYPE_SETTING, null, 'blog' . $type); 5149 } 5150 } 5151 } 5152 } 5153 5154 // Add the messages link. 5155 // It is context based so can appear in the user's profile and in course participants information. 5156 if (!empty($CFG->messaging)) { 5157 $messageargs = array('user1' => $USER->id); 5158 if ($USER->id != $user->id) { 5159 $messageargs['user2'] = $user->id; 5160 } 5161 $url = new moodle_url('/message/index.php', $messageargs); 5162 $mainpage->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages'); 5163 } 5164 5165 // Add the "My private files" link. 5166 // This link doesn't have a unique display for course context so only display it under the user's profile. 5167 if ($issitecourse && $iscurrentuser && has_capability('moodle/user:manageownfiles', $usercontext)) { 5168 $url = new moodle_url('/user/files.php'); 5169 $mainpage->add(get_string('privatefiles'), $url, self::TYPE_SETTING, null, 'privatefiles'); 5170 } 5171 5172 // Add a node to view the users notes if permitted. 5173 if (!empty($CFG->enablenotes) && 5174 has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $coursecontext)) { 5175 $url = new moodle_url('/notes/index.php', array('user' => $user->id)); 5176 if ($coursecontext->instanceid != SITEID) { 5177 $url->param('course', $coursecontext->instanceid); 5178 } 5179 $profilenode->add(get_string('notes', 'notes'), $url); 5180 } 5181 5182 // Show the grades node. 5183 if (($issitecourse && $iscurrentuser) || has_capability('moodle/user:viewdetails', $usercontext)) { 5184 require_once($CFG->dirroot . '/user/lib.php'); 5185 // Set the grades node to link to the "Grades" page. 5186 if ($course->id == SITEID) { 5187 $url = user_mygrades_url($user->id, $course->id); 5188 } else { // Otherwise we are in a course and should redirect to the user grade report (Activity report version). 5189 $url = new moodle_url('/course/user.php', array('mode' => 'grade', 'id' => $course->id, 'user' => $user->id)); 5190 } 5191 $mainpage->add(get_string('grades', 'grades'), $url, self::TYPE_SETTING, null, 'mygrades'); 5192 } 5193 5194 // Let plugins hook into user navigation. 5195 $pluginsfunction = get_plugins_with_function('extend_navigation_user', 'lib.php'); 5196 foreach ($pluginsfunction as $plugintype => $plugins) { 5197 if ($plugintype != 'report') { 5198 foreach ($plugins as $pluginfunction) { 5199 $pluginfunction($profilenode, $user, $usercontext, $course, $coursecontext); 5200 } 5201 } 5202 } 5203 5204 $usersetting = navigation_node::create(get_string('preferences', 'moodle'), $prefurl, self::TYPE_CONTAINER, null, $key); 5205 $mainpage->add_node($usersetting); 5206 } else { 5207 $usersetting = $this->add(get_string('preferences', 'moodle'), $prefurl, self::TYPE_CONTAINER, null, $key); 5208 $usersetting->display = false; 5209 } 5210 $usersetting->id = 'usersettings'; 5211 5212 // Check if the user has been deleted. 5213 if ($user->deleted) { 5214 if (!has_capability('moodle/user:update', $coursecontext)) { 5215 // We can't edit the user so just show the user deleted message. 5216 $usersetting->add(get_string('userdeleted'), null, self::TYPE_SETTING); 5217 } else { 5218 // We can edit the user so show the user deleted message and link it to the profile. 5219 if ($course->id == $SITE->id) { 5220 $profileurl = new moodle_url('/user/profile.php', array('id'=>$user->id)); 5221 } else { 5222 $profileurl = new moodle_url('/user/view.php', array('id'=>$user->id, 'course'=>$course->id)); 5223 } 5224 $usersetting->add(get_string('userdeleted'), $profileurl, self::TYPE_SETTING); 5225 } 5226 return true; 5227 } 5228 5229 $userauthplugin = false; 5230 if (!empty($user->auth)) { 5231 $userauthplugin = get_auth_plugin($user->auth); 5232 } 5233 5234 $useraccount = $usersetting->add(get_string('useraccount'), null, self::TYPE_CONTAINER, null, 'useraccount'); 5235 5236 // Add the profile edit link. 5237 if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) { 5238 if (($currentuser || is_siteadmin($USER) || !is_siteadmin($user)) && 5239 has_capability('moodle/user:update', $systemcontext)) { 5240 $url = new moodle_url('/user/editadvanced.php', array('id'=>$user->id, 'course'=>$course->id)); 5241 $useraccount->add(get_string('editmyprofile'), $url, self::TYPE_SETTING, null, 'editprofile'); 5242 } else if ((has_capability('moodle/user:editprofile', $usercontext) && !is_siteadmin($user)) || 5243 ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext))) { 5244 if ($userauthplugin && $userauthplugin->can_edit_profile()) { 5245 $url = $userauthplugin->edit_profile_url(); 5246 if (empty($url)) { 5247 $url = new moodle_url('/user/edit.php', array('id'=>$user->id, 'course'=>$course->id)); 5248 } 5249 $useraccount->add(get_string('editmyprofile'), $url, self::TYPE_SETTING, null, 'editprofile'); 5250 } 5251 } 5252 } 5253 5254 // Change password link. 5255 if ($userauthplugin && $currentuser && !\core\session\manager::is_loggedinas() && !isguestuser() && 5256 has_capability('moodle/user:changeownpassword', $systemcontext) && $userauthplugin->can_change_password()) { 5257 $passwordchangeurl = $userauthplugin->change_password_url(); 5258 if (empty($passwordchangeurl)) { 5259 $passwordchangeurl = new moodle_url('/login/change_password.php', array('id'=>$course->id)); 5260 } 5261 $useraccount->add(get_string("changepassword"), $passwordchangeurl, self::TYPE_SETTING, null, 'changepassword'); 5262 } 5263 5264 // Default homepage. 5265 $defaulthomepageuser = (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER)); 5266 if (isloggedin() && !isguestuser($user) && $defaulthomepageuser) { 5267 if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || 5268 has_capability('moodle/user:editprofile', $usercontext)) { 5269 $url = new moodle_url('/user/defaulthomepage.php', ['id' => $user->id]); 5270 $useraccount->add(get_string('defaulthomepageuser'), $url, self::TYPE_SETTING, null, 'defaulthomepageuser'); 5271 } 5272 } 5273 5274 if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) { 5275 if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || 5276 has_capability('moodle/user:editprofile', $usercontext)) { 5277 $url = new moodle_url('/user/language.php', array('id' => $user->id, 'course' => $course->id)); 5278 $useraccount->add(get_string('preferredlanguage'), $url, self::TYPE_SETTING, null, 'preferredlanguage'); 5279 } 5280 } 5281 $pluginmanager = core_plugin_manager::instance(); 5282 $enabled = $pluginmanager->get_enabled_plugins('mod'); 5283 if (isset($enabled['forum']) && isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) { 5284 if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || 5285 has_capability('moodle/user:editprofile', $usercontext)) { 5286 $url = new moodle_url('/user/forum.php', array('id' => $user->id, 'course' => $course->id)); 5287 $useraccount->add(get_string('forumpreferences'), $url, self::TYPE_SETTING); 5288 } 5289 } 5290 $editors = editors_get_enabled(); 5291 if (count($editors) > 1) { 5292 if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) { 5293 if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || 5294 has_capability('moodle/user:editprofile', $usercontext)) { 5295 $url = new moodle_url('/user/editor.php', array('id' => $user->id, 'course' => $course->id)); 5296 $useraccount->add(get_string('editorpreferences'), $url, self::TYPE_SETTING); 5297 } 5298 } 5299 } 5300 5301 // Add "Calendar preferences" link. 5302 if (isloggedin() && !isguestuser($user)) { 5303 if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || 5304 has_capability('moodle/user:editprofile', $usercontext)) { 5305 $url = new moodle_url('/user/calendar.php', array('id' => $user->id)); 5306 $useraccount->add(get_string('calendarpreferences', 'calendar'), $url, self::TYPE_SETTING, null, 'preferredcalendar'); 5307 } 5308 } 5309 5310 // Add "Content bank preferences" link. 5311 if (isloggedin() && !isguestuser($user)) { 5312 if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || 5313 has_capability('moodle/user:editprofile', $usercontext)) { 5314 $url = new moodle_url('/user/contentbank.php', ['id' => $user->id]); 5315 $useraccount->add(get_string('contentbankpreferences', 'core_contentbank'), $url, self::TYPE_SETTING, 5316 null, 'contentbankpreferences'); 5317 } 5318 } 5319 5320 // View the roles settings. 5321 if (has_any_capability(['moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', 5322 'moodle/role:manage'], $usercontext)) { 5323 $roles = $usersetting->add(get_string('roles'), null, self::TYPE_SETTING); 5324 5325 $url = new moodle_url('/admin/roles/usersroles.php', ['userid' => $user->id, 'courseid' => $course->id]); 5326 $roles->add(get_string('thisusersroles', 'role'), $url, self::TYPE_SETTING); 5327 5328 $assignableroles = get_assignable_roles($usercontext, ROLENAME_BOTH); 5329 5330 if (!empty($assignableroles)) { 5331 $url = new moodle_url('/admin/roles/assign.php', 5332 array('contextid' => $usercontext->id, 'userid' => $user->id, 'courseid' => $course->id)); 5333 $roles->add(get_string('assignrolesrelativetothisuser', 'role'), $url, self::TYPE_SETTING); 5334 } 5335 5336 if (has_capability('moodle/role:review', $usercontext) || count(get_overridable_roles($usercontext, ROLENAME_BOTH))>0) { 5337 $url = new moodle_url('/admin/roles/permissions.php', 5338 array('contextid' => $usercontext->id, 'userid' => $user->id, 'courseid' => $course->id)); 5339 $roles->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING); 5340 } 5341 5342 $url = new moodle_url('/admin/roles/check.php', 5343 array('contextid' => $usercontext->id, 'userid' => $user->id, 'courseid' => $course->id)); 5344 $roles->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING); 5345 } 5346 5347 // Repositories. 5348 if (!$this->cache->cached('contexthasrepos'.$usercontext->id)) { 5349 require_once($CFG->dirroot . '/repository/lib.php'); 5350 $editabletypes = repository::get_editable_types($usercontext); 5351 $haseditabletypes = !empty($editabletypes); 5352 unset($editabletypes); 5353 $this->cache->set('contexthasrepos'.$usercontext->id, $haseditabletypes); 5354 } else { 5355 $haseditabletypes = $this->cache->{'contexthasrepos'.$usercontext->id}; 5356 } 5357 if ($haseditabletypes) { 5358 $repositories = $usersetting->add(get_string('repositories', 'repository'), null, self::TYPE_SETTING); 5359 $repositories->add(get_string('manageinstances', 'repository'), new moodle_url('/repository/manage_instances.php', 5360 array('contextid' => $usercontext->id))); 5361 } 5362 5363 // Portfolio. 5364 if ($currentuser && !empty($CFG->enableportfolios) && has_capability('moodle/portfolio:export', $systemcontext)) { 5365 require_once($CFG->libdir . '/portfoliolib.php'); 5366 if (portfolio_has_visible_instances()) { 5367 $portfolio = $usersetting->add(get_string('portfolios', 'portfolio'), null, self::TYPE_SETTING); 5368 5369 $url = new moodle_url('/user/portfolio.php', array('courseid'=>$course->id)); 5370 $portfolio->add(get_string('configure', 'portfolio'), $url, self::TYPE_SETTING); 5371 5372 $url = new moodle_url('/user/portfoliologs.php', array('courseid'=>$course->id)); 5373 $portfolio->add(get_string('logs', 'portfolio'), $url, self::TYPE_SETTING); 5374 } 5375 } 5376 5377 $enablemanagetokens = false; 5378 if (!empty($CFG->enablerssfeeds)) { 5379 $enablemanagetokens = true; 5380 } else if (!is_siteadmin($USER->id) 5381 && !empty($CFG->enablewebservices) 5382 && has_capability('moodle/webservice:createtoken', context_system::instance()) ) { 5383 $enablemanagetokens = true; 5384 } 5385 // Security keys. 5386 if ($currentuser && $enablemanagetokens) { 5387 $url = new moodle_url('/user/managetoken.php'); 5388 $useraccount->add(get_string('securitykeys', 'webservice'), $url, self::TYPE_SETTING); 5389 } 5390 5391 // Messaging. 5392 if (($currentuser && has_capability('moodle/user:editownmessageprofile', $systemcontext)) || (!isguestuser($user) && 5393 has_capability('moodle/user:editmessageprofile', $usercontext) && !is_primary_admin($user->id))) { 5394 $messagingurl = new moodle_url('/message/edit.php', array('id' => $user->id)); 5395 $notificationsurl = new moodle_url('/message/notificationpreferences.php', array('userid' => $user->id)); 5396 $useraccount->add(get_string('messagepreferences', 'message'), $messagingurl, self::TYPE_SETTING); 5397 $useraccount->add(get_string('notificationpreferences', 'message'), $notificationsurl, self::TYPE_SETTING); 5398 } 5399 5400 // Blogs. 5401 if ($currentuser && !empty($CFG->enableblogs)) { 5402 $blog = $usersetting->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER, null, 'blogs'); 5403 if (has_capability('moodle/blog:view', $systemcontext)) { 5404 $blog->add(get_string('preferences', 'blog'), new moodle_url('/blog/preferences.php'), 5405 navigation_node::TYPE_SETTING); 5406 } 5407 if (!empty($CFG->useexternalblogs) && $CFG->maxexternalblogsperuser > 0 && 5408 has_capability('moodle/blog:manageexternal', $systemcontext)) { 5409 $blog->add(get_string('externalblogs', 'blog'), new moodle_url('/blog/external_blogs.php'), 5410 navigation_node::TYPE_SETTING); 5411 $blog->add(get_string('addnewexternalblog', 'blog'), new moodle_url('/blog/external_blog_edit.php'), 5412 navigation_node::TYPE_SETTING); 5413 } 5414 // Remove the blog node if empty. 5415 $blog->trim_if_empty(); 5416 } 5417 5418 // Badges. 5419 if ($currentuser && !empty($CFG->enablebadges)) { 5420 $badges = $usersetting->add(get_string('badges'), null, navigation_node::TYPE_CONTAINER, null, 'badges'); 5421 if (has_capability('moodle/badges:manageownbadges', $usercontext)) { 5422 $url = new moodle_url('/badges/mybadges.php'); 5423 $badges->add(get_string('managebadges', 'badges'), $url, self::TYPE_SETTING); 5424 } 5425 $badges->add(get_string('preferences', 'badges'), new moodle_url('/badges/preferences.php'), 5426 navigation_node::TYPE_SETTING); 5427 if (!empty($CFG->badges_allowexternalbackpack)) { 5428 $badges->add(get_string('backpackdetails', 'badges'), new moodle_url('/badges/mybackpack.php'), 5429 navigation_node::TYPE_SETTING); 5430 } 5431 } 5432 5433 // Let plugins hook into user settings navigation. 5434 $pluginsfunction = get_plugins_with_function('extend_navigation_user_settings', 'lib.php'); 5435 foreach ($pluginsfunction as $plugintype => $plugins) { 5436 foreach ($plugins as $pluginfunction) { 5437 $pluginfunction($usersetting, $user, $usercontext, $course, $coursecontext); 5438 } 5439 } 5440 5441 return $usersetting; 5442 } 5443 5444 /** 5445 * Loads block specific settings in the navigation 5446 * 5447 * @return navigation_node 5448 */ 5449 protected function load_block_settings() { 5450 global $CFG; 5451 5452 $blocknode = $this->add($this->context->get_context_name(), null, self::TYPE_SETTING, null, 'blocksettings'); 5453 $blocknode->force_open(); 5454 5455 // Assign local roles 5456 if (get_assignable_roles($this->context, ROLENAME_ORIGINAL)) { 5457 $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $this->context->id)); 5458 $blocknode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING, null, 5459 'roles', new pix_icon('i/assignroles', '')); 5460 } 5461 5462 // Override roles 5463 if (has_capability('moodle/role:review', $this->context) or count(get_overridable_roles($this->context))>0) { 5464 $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid'=>$this->context->id)); 5465 $blocknode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null, 5466 'permissions', new pix_icon('i/permissions', '')); 5467 } 5468 // Check role permissions 5469 if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $this->context)) { 5470 $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->context->id)); 5471 $blocknode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 5472 'checkpermissions', new pix_icon('i/checkpermissions', '')); 5473 } 5474 5475 // Add the context locking node. 5476 $this->add_context_locking_node($blocknode, $this->context); 5477 5478 return $blocknode; 5479 } 5480 5481 /** 5482 * Loads category specific settings in the navigation 5483 * 5484 * @return navigation_node 5485 */ 5486 protected function load_category_settings() { 5487 global $CFG; 5488 5489 // We can land here while being in the context of a block, in which case we 5490 // should get the parent context which should be the category one. See self::initialise(). 5491 if ($this->context->contextlevel == CONTEXT_BLOCK) { 5492 $catcontext = $this->context->get_parent_context(); 5493 } else { 5494 $catcontext = $this->context; 5495 } 5496 5497 // Let's make sure that we always have the right context when getting here. 5498 if ($catcontext->contextlevel != CONTEXT_COURSECAT) { 5499 throw new coding_exception('Unexpected context while loading category settings.'); 5500 } 5501 5502 $categorynodetype = navigation_node::TYPE_CONTAINER; 5503 $categorynode = $this->add($catcontext->get_context_name(), null, $categorynodetype, null, 'categorysettings'); 5504 $categorynode->nodetype = navigation_node::NODETYPE_BRANCH; 5505 $categorynode->force_open(); 5506 5507 if (can_edit_in_category($catcontext->instanceid)) { 5508 $url = new moodle_url('/course/management.php', array('categoryid' => $catcontext->instanceid)); 5509 $editstring = get_string('managecategorythis'); 5510 $node = $categorynode->add($editstring, $url, self::TYPE_SETTING, null, 'managecategory', new pix_icon('i/edit', '')); 5511 $node->set_show_in_secondary_navigation(false); 5512 } 5513 5514 if (has_capability('moodle/category:manage', $catcontext)) { 5515 $editurl = new moodle_url('/course/editcategory.php', array('id' => $catcontext->instanceid)); 5516 $categorynode->add(get_string('settings'), $editurl, self::TYPE_SETTING, null, 'edit', new pix_icon('i/edit', '')); 5517 5518 $addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $catcontext->instanceid)); 5519 $categorynode->add(get_string('addsubcategory'), $addsubcaturl, self::TYPE_SETTING, null, 5520 'addsubcat', new pix_icon('i/withsubcat', ''))->set_show_in_secondary_navigation(false); 5521 } 5522 5523 // Assign local roles 5524 $assignableroles = get_assignable_roles($catcontext); 5525 if (!empty($assignableroles)) { 5526 $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $catcontext->id)); 5527 $categorynode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', '')); 5528 } 5529 5530 // Override roles 5531 if (has_capability('moodle/role:review', $catcontext) or count(get_overridable_roles($catcontext)) > 0) { 5532 $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid' => $catcontext->id)); 5533 $categorynode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null, 'permissions', new pix_icon('i/permissions', '')); 5534 } 5535 // Check role permissions 5536 if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 5537 'moodle/role:override', 'moodle/role:assign'), $catcontext)) { 5538 $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid' => $catcontext->id)); 5539 $categorynode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'rolecheck', new pix_icon('i/checkpermissions', '')); 5540 } 5541 5542 // Add the context locking node. 5543 $this->add_context_locking_node($categorynode, $catcontext); 5544 5545 // Cohorts 5546 if (has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $catcontext)) { 5547 $categorynode->add(get_string('cohorts', 'cohort'), new moodle_url('/cohort/index.php', 5548 array('contextid' => $catcontext->id)), self::TYPE_SETTING, null, 'cohort', new pix_icon('i/cohort', '')); 5549 } 5550 5551 // Manage filters 5552 if (has_capability('moodle/filter:manage', $catcontext) && count(filter_get_available_in_context($catcontext)) > 0) { 5553 $url = new moodle_url('/filter/manage.php', array('contextid' => $catcontext->id)); 5554 $categorynode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, 'filters', new pix_icon('i/filter', '')); 5555 } 5556 5557 // Restore. 5558 if (has_capability('moodle/restore:restorecourse', $catcontext)) { 5559 $url = new moodle_url('/backup/restorefile.php', array('contextid' => $catcontext->id)); 5560 $categorynode->add(get_string('restorecourse', 'admin'), $url, self::TYPE_SETTING, null, 'restorecourse', new pix_icon('i/restore', '')); 5561 } 5562 5563 // Let plugins hook into category settings navigation. 5564 $pluginsfunction = get_plugins_with_function('extend_navigation_category_settings', 'lib.php'); 5565 foreach ($pluginsfunction as $plugintype => $plugins) { 5566 foreach ($plugins as $pluginfunction) { 5567 $pluginfunction($categorynode, $catcontext); 5568 } 5569 } 5570 5571 $cb = new contentbank(); 5572 if ($cb->is_context_allowed($catcontext) 5573 && has_capability('moodle/contentbank:access', $catcontext)) { 5574 $url = new \moodle_url('/contentbank/index.php', ['contextid' => $catcontext->id]); 5575 $categorynode->add(get_string('contentbank'), $url, self::TYPE_CUSTOM, null, 5576 'contentbank', new \pix_icon('i/contentbank', '')); 5577 } 5578 5579 return $categorynode; 5580 } 5581 5582 /** 5583 * Determine whether the user is assuming another role 5584 * 5585 * This function checks to see if the user is assuming another role by means of 5586 * role switching. In doing this we compare each RSW key (context path) against 5587 * the current context path. This ensures that we can provide the switching 5588 * options against both the course and any page shown under the course. 5589 * 5590 * @return bool|int The role(int) if the user is in another role, false otherwise 5591 */ 5592 protected function in_alternative_role() { 5593 global $USER; 5594 if (!empty($USER->access['rsw']) && is_array($USER->access['rsw'])) { 5595 if (!empty($this->page->context) && !empty($USER->access['rsw'][$this->page->context->path])) { 5596 return $USER->access['rsw'][$this->page->context->path]; 5597 } 5598 foreach ($USER->access['rsw'] as $key=>$role) { 5599 if (strpos($this->context->path,$key)===0) { 5600 return $role; 5601 } 5602 } 5603 } 5604 return false; 5605 } 5606 5607 /** 5608 * This function loads all of the front page settings into the settings navigation. 5609 * This function is called when the user is on the front page, or $COURSE==$SITE 5610 * @param bool $forceopen (optional) 5611 * @return navigation_node 5612 */ 5613 protected function load_front_page_settings($forceopen = false) { 5614 global $SITE, $CFG; 5615 require_once($CFG->dirroot . '/course/lib.php'); 5616 5617 $course = clone($SITE); 5618 $coursecontext = context_course::instance($course->id); // Course context 5619 $adminoptions = course_get_user_administration_options($course, $coursecontext); 5620 5621 $frontpage = $this->add(get_string('frontpagesettings'), null, self::TYPE_SETTING, null, 'frontpage'); 5622 if ($forceopen) { 5623 $frontpage->force_open(); 5624 } 5625 $frontpage->id = 'frontpagesettings'; 5626 5627 if ($this->page->user_allowed_editing() && !$this->page->theme->haseditswitch) { 5628 5629 // Add the turn on/off settings 5630 $url = new moodle_url('/course/view.php', array('id'=>$course->id, 'sesskey'=>sesskey())); 5631 if ($this->page->user_is_editing()) { 5632 $url->param('edit', 'off'); 5633 $editstring = get_string('turneditingoff'); 5634 } else { 5635 $url->param('edit', 'on'); 5636 $editstring = get_string('turneditingon'); 5637 } 5638 $frontpage->add($editstring, $url, self::TYPE_SETTING, null, null, new pix_icon('i/edit', '')); 5639 } 5640 5641 if ($adminoptions->update) { 5642 // Add the course settings link 5643 $url = new moodle_url('/admin/settings.php', array('section'=>'frontpagesettings')); 5644 $frontpage->add(get_string('settings'), $url, self::TYPE_SETTING, null, 5645 'editsettings', new pix_icon('i/settings', '')); 5646 } 5647 5648 // add enrol nodes 5649 enrol_add_course_navigation($frontpage, $course); 5650 5651 // Manage filters 5652 if ($adminoptions->filters) { 5653 $url = new moodle_url('/filter/manage.php', array('contextid'=>$coursecontext->id)); 5654 $frontpage->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, 5655 null, 'filtermanagement', new pix_icon('i/filter', '')); 5656 } 5657 5658 // View course reports. 5659 if ($adminoptions->reports) { 5660 $frontpagenav = $frontpage->add(get_string('reports'), new moodle_url('/report/view.php', 5661 ['courseid' => $coursecontext->instanceid]), 5662 self::TYPE_CONTAINER, null, 'coursereports', 5663 new pix_icon('i/stats', '')); 5664 $coursereports = core_component::get_plugin_list('coursereport'); 5665 foreach ($coursereports as $report=>$dir) { 5666 $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php'; 5667 if (file_exists($libfile)) { 5668 require_once($libfile); 5669 $reportfunction = $report.'_report_extend_navigation'; 5670 if (function_exists($report.'_report_extend_navigation')) { 5671 $reportfunction($frontpagenav, $course, $coursecontext); 5672 } 5673 } 5674 } 5675 5676 $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php'); 5677 foreach ($reports as $reportfunction) { 5678 $reportfunction($frontpagenav, $course, $coursecontext); 5679 } 5680 5681 if (!$frontpagenav->has_children()) { 5682 $frontpagenav->remove(); 5683 } 5684 } 5685 5686 // Backup this course 5687 if ($adminoptions->backup) { 5688 $url = new moodle_url('/backup/backup.php', array('id'=>$course->id)); 5689 $frontpage->add(get_string('backup'), $url, self::TYPE_SETTING, null, 'backup', new pix_icon('i/backup', '')); 5690 } 5691 5692 // Restore to this course 5693 if ($adminoptions->restore) { 5694 $url = new moodle_url('/backup/restorefile.php', array('contextid'=>$coursecontext->id)); 5695 $frontpage->add(get_string('restore'), $url, self::TYPE_SETTING, null, 'restore', new pix_icon('i/restore', '')); 5696 } 5697 5698 // Questions 5699 require_once($CFG->libdir . '/questionlib.php'); 5700 question_extend_settings_navigation($frontpage, $coursecontext)->trim_if_empty(); 5701 5702 // Manage files 5703 if ($adminoptions->files) { 5704 //hiden in new installs 5705 $url = new moodle_url('/files/index.php', array('contextid'=>$coursecontext->id)); 5706 $frontpage->add(get_string('sitelegacyfiles'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/folder', '')); 5707 } 5708 5709 // Let plugins hook into frontpage navigation. 5710 $pluginsfunction = get_plugins_with_function('extend_navigation_frontpage', 'lib.php'); 5711 foreach ($pluginsfunction as $plugintype => $plugins) { 5712 foreach ($plugins as $pluginfunction) { 5713 $pluginfunction($frontpage, $course, $coursecontext); 5714 } 5715 } 5716 5717 return $frontpage; 5718 } 5719 5720 /** 5721 * This function gives local plugins an opportunity to modify the settings navigation. 5722 */ 5723 protected function load_local_plugin_settings() { 5724 5725 foreach (get_plugin_list_with_function('local', 'extend_settings_navigation') as $function) { 5726 $function($this, $this->context); 5727 } 5728 } 5729 5730 /** 5731 * This function marks the cache as volatile so it is cleared during shutdown 5732 */ 5733 public function clear_cache() { 5734 $this->cache->volatile(); 5735 } 5736 5737 /** 5738 * Checks to see if there are child nodes available in the specific user's preference node. 5739 * If so, then they have the appropriate permissions view this user's preferences. 5740 * 5741 * @since Moodle 2.9.3 5742 * @param int $userid The user's ID. 5743 * @return bool True if child nodes exist to view, otherwise false. 5744 */ 5745 public function can_view_user_preferences($userid) { 5746 if (is_siteadmin()) { 5747 return true; 5748 } 5749 // See if any nodes are present in the preferences section for this user. 5750 $preferencenode = $this->find('userviewingsettings' . $userid, null); 5751 if ($preferencenode && $preferencenode->has_children()) { 5752 // Run through each child node. 5753 foreach ($preferencenode->children as $childnode) { 5754 // If the child node has children then this user has access to a link in the preferences page. 5755 if ($childnode->has_children()) { 5756 return true; 5757 } 5758 } 5759 } 5760 // No links found for the user to access on the preferences page. 5761 return false; 5762 } 5763 } 5764 5765 /** 5766 * Class used to populate site admin navigation for ajax. 5767 * 5768 * @package core 5769 * @category navigation 5770 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> 5771 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 5772 */ 5773 class settings_navigation_ajax extends settings_navigation { 5774 /** 5775 * Constructs the navigation for use in an AJAX request 5776 * 5777 * @param moodle_page $page 5778 */ 5779 public function __construct(moodle_page &$page) { 5780 $this->page = $page; 5781 $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME); 5782 $this->children = new navigation_node_collection(); 5783 $this->initialise(); 5784 } 5785 5786 /** 5787 * Initialise the site admin navigation. 5788 */ 5789 public function initialise() { 5790 if ($this->initialised || during_initial_install()) { 5791 return false; 5792 } 5793 $this->context = $this->page->context; 5794 $this->load_administration_settings(); 5795 5796 // Check if local plugins is adding node to site admin. 5797 $this->load_local_plugin_settings(); 5798 5799 $this->initialised = true; 5800 } 5801 } 5802 5803 /** 5804 * Simple class used to output a navigation branch in XML 5805 * 5806 * @package core 5807 * @category navigation 5808 * @copyright 2009 Sam Hemelryk 5809 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 5810 */ 5811 class navigation_json { 5812 /** @var array An array of different node types */ 5813 protected $nodetype = array('node','branch'); 5814 /** @var array An array of node keys and types */ 5815 protected $expandable = array(); 5816 /** 5817 * Turns a branch and all of its children into XML 5818 * 5819 * @param navigation_node $branch 5820 * @return string XML string 5821 */ 5822 public function convert($branch) { 5823 $xml = $this->convert_child($branch); 5824 return $xml; 5825 } 5826 /** 5827 * Set the expandable items in the array so that we have enough information 5828 * to attach AJAX events 5829 * @param array $expandable 5830 */ 5831 public function set_expandable($expandable) { 5832 foreach ($expandable as $node) { 5833 $this->expandable[$node['key'].':'.$node['type']] = $node; 5834 } 5835 } 5836 /** 5837 * Recusively converts a child node and its children to XML for output 5838 * 5839 * @param navigation_node $child The child to convert 5840 * @param int $depth Pointlessly used to track the depth of the XML structure 5841 * @return string JSON 5842 */ 5843 protected function convert_child($child, $depth=1) { 5844 if (!$child->display) { 5845 return ''; 5846 } 5847 $attributes = array(); 5848 $attributes['id'] = $child->id; 5849 $attributes['name'] = (string)$child->text; // This can be lang_string object so typecast it. 5850 $attributes['type'] = $child->type; 5851 $attributes['key'] = $child->key; 5852 $attributes['class'] = $child->get_css_type(); 5853 $attributes['requiresajaxloading'] = $child->requiresajaxloading; 5854 5855 if ($child->icon instanceof pix_icon) { 5856 $attributes['icon'] = array( 5857 'component' => $child->icon->component, 5858 'pix' => $child->icon->pix, 5859 ); 5860 foreach ($child->icon->attributes as $key=>$value) { 5861 if ($key == 'class') { 5862 $attributes['icon']['classes'] = explode(' ', $value); 5863 } else if (!array_key_exists($key, $attributes['icon'])) { 5864 $attributes['icon'][$key] = $value; 5865 } 5866 5867 } 5868 } else if (!empty($child->icon)) { 5869 $attributes['icon'] = (string)$child->icon; 5870 } 5871 5872 if ($child->forcetitle || $child->title !== $child->text) { 5873 $attributes['title'] = htmlentities($child->title ?? '', ENT_QUOTES, 'UTF-8'); 5874 } 5875 if (array_key_exists($child->key.':'.$child->type, $this->expandable)) { 5876 $attributes['expandable'] = $child->key; 5877 $child->add_class($this->expandable[$child->key.':'.$child->type]['id']); 5878 } 5879 5880 if (count($child->classes)>0) { 5881 $attributes['class'] .= ' '.join(' ',$child->classes); 5882 } 5883 if (is_string($child->action)) { 5884 $attributes['link'] = $child->action; 5885 } else if ($child->action instanceof moodle_url) { 5886 $attributes['link'] = $child->action->out(); 5887 } else if ($child->action instanceof action_link) { 5888 $attributes['link'] = $child->action->url->out(); 5889 } 5890 $attributes['hidden'] = ($child->hidden); 5891 $attributes['haschildren'] = ($child->children->count()>0 || $child->type == navigation_node::TYPE_CATEGORY); 5892 $attributes['haschildren'] = $attributes['haschildren'] || $child->type == navigation_node::TYPE_MY_CATEGORY; 5893 5894 if ($child->children->count() > 0) { 5895 $attributes['children'] = array(); 5896 foreach ($child->children as $subchild) { 5897 $attributes['children'][] = $this->convert_child($subchild, $depth+1); 5898 } 5899 } 5900 5901 if ($depth > 1) { 5902 return $attributes; 5903 } else { 5904 return json_encode($attributes); 5905 } 5906 } 5907 } 5908 5909 /** 5910 * The cache class used by global navigation and settings navigation. 5911 * 5912 * It is basically an easy access point to session with a bit of smarts to make 5913 * sure that the information that is cached is valid still. 5914 * 5915 * Example use: 5916 * <code php> 5917 * if (!$cache->viewdiscussion()) { 5918 * // Code to do stuff and produce cachable content 5919 * $cache->viewdiscussion = has_capability('mod/forum:viewdiscussion', $coursecontext); 5920 * } 5921 * $content = $cache->viewdiscussion; 5922 * </code> 5923 * 5924 * @package core 5925 * @category navigation 5926 * @copyright 2009 Sam Hemelryk 5927 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 5928 */ 5929 class navigation_cache { 5930 /** @var int represents the time created */ 5931 protected $creation; 5932 /** @var array An array of session keys */ 5933 protected $session; 5934 /** 5935 * The string to use to segregate this particular cache. It can either be 5936 * unique to start a fresh cache or if you want to share a cache then make 5937 * it the string used in the original cache. 5938 * @var string 5939 */ 5940 protected $area; 5941 /** @var int a time that the information will time out */ 5942 protected $timeout; 5943 /** @var stdClass The current context */ 5944 protected $currentcontext; 5945 /** @var int cache time information */ 5946 const CACHETIME = 0; 5947 /** @var int cache user id */ 5948 const CACHEUSERID = 1; 5949 /** @var int cache value */ 5950 const CACHEVALUE = 2; 5951 /** @var null|array An array of navigation cache areas to expire on shutdown */ 5952 public static $volatilecaches; 5953 5954 /** 5955 * Contructor for the cache. Requires two arguments 5956 * 5957 * @param string $area The string to use to segregate this particular cache 5958 * it can either be unique to start a fresh cache or if you want 5959 * to share a cache then make it the string used in the original 5960 * cache 5961 * @param int $timeout The number of seconds to time the information out after 5962 */ 5963 public function __construct($area, $timeout=1800) { 5964 $this->creation = time(); 5965 $this->area = $area; 5966 $this->timeout = time() - $timeout; 5967 if (rand(0,100) === 0) { 5968 $this->garbage_collection(); 5969 } 5970 } 5971 5972 /** 5973 * Used to set up the cache within the SESSION. 5974 * 5975 * This is called for each access and ensure that we don't put anything into the session before 5976 * it is required. 5977 */ 5978 protected function ensure_session_cache_initialised() { 5979 global $SESSION; 5980 if (empty($this->session)) { 5981 if (!isset($SESSION->navcache)) { 5982 $SESSION->navcache = new stdClass; 5983 } 5984 if (!isset($SESSION->navcache->{$this->area})) { 5985 $SESSION->navcache->{$this->area} = array(); 5986 } 5987 $this->session = &$SESSION->navcache->{$this->area}; // pointer to array, =& is correct here 5988 } 5989 } 5990 5991 /** 5992 * Magic Method to retrieve something by simply calling using = cache->key 5993 * 5994 * @param mixed $key The identifier for the information you want out again 5995 * @return void|mixed Either void or what ever was put in 5996 */ 5997 public function __get($key) { 5998 if (!$this->cached($key)) { 5999 return; 6000 } 6001 $information = $this->session[$key][self::CACHEVALUE]; 6002 return unserialize($information); 6003 } 6004 6005 /** 6006 * Magic method that simply uses {@link set();} to store something in the cache 6007 * 6008 * @param string|int $key 6009 * @param mixed $information 6010 */ 6011 public function __set($key, $information) { 6012 $this->set($key, $information); 6013 } 6014 6015 /** 6016 * Sets some information against the cache (session) for later retrieval 6017 * 6018 * @param string|int $key 6019 * @param mixed $information 6020 */ 6021 public function set($key, $information) { 6022 global $USER; 6023 $this->ensure_session_cache_initialised(); 6024 $information = serialize($information); 6025 $this->session[$key]= array(self::CACHETIME=>time(), self::CACHEUSERID=>$USER->id, self::CACHEVALUE=>$information); 6026 } 6027 /** 6028 * Check the existence of the identifier in the cache 6029 * 6030 * @param string|int $key 6031 * @return bool 6032 */ 6033 public function cached($key) { 6034 global $USER; 6035 $this->ensure_session_cache_initialised(); 6036 if (!array_key_exists($key, $this->session) || !is_array($this->session[$key]) || $this->session[$key][self::CACHEUSERID]!=$USER->id || $this->session[$key][self::CACHETIME] < $this->timeout) { 6037 return false; 6038 } 6039 return true; 6040 } 6041 /** 6042 * Compare something to it's equivilant in the cache 6043 * 6044 * @param string $key 6045 * @param mixed $value 6046 * @param bool $serialise Whether to serialise the value before comparison 6047 * this should only be set to false if the value is already 6048 * serialised 6049 * @return bool If the value is the same false if it is not set or doesn't match 6050 */ 6051 public function compare($key, $value, $serialise = true) { 6052 if ($this->cached($key)) { 6053 if ($serialise) { 6054 $value = serialize($value); 6055 } 6056 if ($this->session[$key][self::CACHEVALUE] === $value) { 6057 return true; 6058 } 6059 } 6060 return false; 6061 } 6062 /** 6063 * Wipes the entire cache, good to force regeneration 6064 */ 6065 public function clear() { 6066 global $SESSION; 6067 unset($SESSION->navcache); 6068 $this->session = null; 6069 } 6070 /** 6071 * Checks all cache entries and removes any that have expired, good ole cleanup 6072 */ 6073 protected function garbage_collection() { 6074 if (empty($this->session)) { 6075 return true; 6076 } 6077 foreach ($this->session as $key=>$cachedinfo) { 6078 if (is_array($cachedinfo) && $cachedinfo[self::CACHETIME]<$this->timeout) { 6079 unset($this->session[$key]); 6080 } 6081 } 6082 } 6083 6084 /** 6085 * Marks the cache as being volatile (likely to change) 6086 * 6087 * Any caches marked as volatile will be destroyed at the on shutdown by 6088 * {@link navigation_node::destroy_volatile_caches()} which is registered 6089 * as a shutdown function if any caches are marked as volatile. 6090 * 6091 * @param bool $setting True to destroy the cache false not too 6092 */ 6093 public function volatile($setting = true) { 6094 if (self::$volatilecaches===null) { 6095 self::$volatilecaches = array(); 6096 core_shutdown_manager::register_function(array('navigation_cache','destroy_volatile_caches')); 6097 } 6098 6099 if ($setting) { 6100 self::$volatilecaches[$this->area] = $this->area; 6101 } else if (array_key_exists($this->area, self::$volatilecaches)) { 6102 unset(self::$volatilecaches[$this->area]); 6103 } 6104 } 6105 6106 /** 6107 * Destroys all caches marked as volatile 6108 * 6109 * This function is static and works in conjunction with the static volatilecaches 6110 * property of navigation cache. 6111 * Because this function is static it manually resets the cached areas back to an 6112 * empty array. 6113 */ 6114 public static function destroy_volatile_caches() { 6115 global $SESSION; 6116 if (is_array(self::$volatilecaches) && count(self::$volatilecaches)>0) { 6117 foreach (self::$volatilecaches as $area) { 6118 $SESSION->navcache->{$area} = array(); 6119 } 6120 } else { 6121 $SESSION->navcache = new stdClass; 6122 } 6123 } 6124 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body