Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [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  namespace core\navigation\views;
  18  
  19  use navigation_node;
  20  use url_select;
  21  use settings_navigation;
  22  
  23  /**
  24   * Class secondary_navigation_view.
  25   *
  26   * The secondary navigation view is a stripped down tweaked version of the
  27   * settings_navigation/navigation
  28   *
  29   * @package     core
  30   * @category    navigation
  31   * @copyright   2021 onwards Peter Dias
  32   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class secondary extends view {
  35      /** @var string $headertitle The header for this particular menu*/
  36      public $headertitle;
  37  
  38      /** @var int The maximum limit of navigation nodes displayed in the secondary navigation */
  39      const MAX_DISPLAYED_NAV_NODES = 5;
  40  
  41      /** @var navigation_node The course overflow node. */
  42      protected $courseoverflownode = null;
  43  
  44      /** @var string The key of the node to set as selected in the course overflow menu, if explicitly set by a page. */
  45      protected $overflowselected = null;
  46  
  47      /**
  48       * Defines the default structure for the secondary nav in a course context.
  49       *
  50       * In a course context, we are curating nodes from the settingsnav and navigation objects.
  51       * The following mapping construct specifies which object we are fetching it from, the type of the node, the key
  52       * and in what order we want the node - defined as per the mockups.
  53       *
  54       * @return array
  55       */
  56      protected function get_default_course_mapping(): array {
  57          $nodes = [];
  58          $nodes['settings'] = [
  59              self::TYPE_CONTAINER => [
  60                  'coursereports' => 3,
  61                  'questionbank' => 4,
  62              ],
  63              self::TYPE_SETTING => [
  64                  'editsettings' => 0,
  65                  'review' => 1.1,
  66                  'manageinstances' => 1.2,
  67                  'groups' => 1.3,
  68                  'override' => 1.4,
  69                  'roles' => 1.5,
  70                  'permissions' => 1.6,
  71                  'otherusers' => 1.7,
  72                  'gradebooksetup' => 2.1,
  73                  'outcomes' => 2.2,
  74                  'coursecompletion' => 6,
  75                  'coursebadges' => 7.1,
  76                  'newbadge' => 7.2,
  77                  'filtermanagement' => 9,
  78                  'unenrolself' => 10,
  79                  'coursetags' => 11,
  80                  'download' => 12,
  81                  'contextlocking' => 13,
  82              ],
  83          ];
  84          $nodes['navigation'] = [
  85              self::TYPE_CONTAINER => [
  86                  'participants' => 1,
  87              ],
  88              self::TYPE_SETTING => [
  89                  'grades' => 2,
  90                  'badgesview' => 7,
  91                  'competencies' => 8,
  92                  'communication' => 14,
  93              ],
  94              self::TYPE_CUSTOM => [
  95                  'contentbank' => 5,
  96                  'participants' => 1, // In site home, 'participants' is classified differently.
  97              ],
  98          ];
  99  
 100          return $nodes;
 101      }
 102  
 103      /**
 104       * Defines the default structure for the secondary nav in a module context.
 105       *
 106       * In a module context, we are curating nodes from the settingsnav object.
 107       * The following mapping construct specifies the type of the node, the key
 108       * and in what order we want the node - defined as per the mockups.
 109       *
 110       * @return array
 111       */
 112      protected function get_default_module_mapping(): array {
 113          return [
 114              self::TYPE_SETTING => [
 115                  'modedit' => 1,
 116                  "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific.
 117                  "mod_{$this->page->activityname}_groupoverrides" => 4,
 118                  'roleassign' => 7.2,
 119                  'filtermanage' => 6,
 120                  'roleoverride' => 7,
 121                  'rolecheck' => 7.1,
 122                  'logreport' => 8,
 123                  'backup' => 9,
 124                  'restore' => 10,
 125                  'competencybreakdown' => 11,
 126                  'sendtomoodlenet' => 16,
 127              ],
 128              self::TYPE_CUSTOM => [
 129                  'advgrading' => 2,
 130                  'contentbank' => 12,
 131              ],
 132          ];
 133      }
 134  
 135      /**
 136       * Defines the default structure for the secondary nav in a category context.
 137       *
 138       * In a category context, we are curating nodes from the settingsnav object.
 139       * The following mapping construct specifies the type of the node, the key
 140       * and in what order we want the node - defined as per the mockups.
 141       *
 142       * @return array
 143       */
 144      protected function get_default_category_mapping(): array {
 145          return [
 146              self::TYPE_SETTING => [
 147                  'edit' => 1,
 148                  'permissions' => 2,
 149                  'roles' => 2.1,
 150                  'rolecheck' => 2.2,
 151              ]
 152          ];
 153      }
 154  
 155      /**
 156       * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
 157       *
 158       * @return array
 159       */
 160      protected function get_default_category_more_menu_nodes(): array {
 161          return ['addsubcat', 'roles', 'permissions', 'contentbank', 'cohort', 'filters', 'restorecourse'];
 162      }
 163      /**
 164       * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
 165       *
 166       * @return array
 167       */
 168      protected function get_default_course_more_menu_nodes(): array {
 169          return [];
 170      }
 171  
 172      /**
 173       * Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.
 174       *
 175       * @return array
 176       */
 177      protected function get_default_module_more_menu_nodes(): array {
 178          return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore',
 179              'competencybreakdown', "mod_{$this->page->activityname}_useroverrides",
 180              "mod_{$this->page->activityname}_groupoverrides"];
 181      }
 182  
 183      /**
 184       * Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.
 185       *
 186       * @return array
 187       */
 188      protected function get_default_admin_more_menu_nodes(): array {
 189          return [];
 190      }
 191  
 192      /**
 193       * Initialise the view based navigation based on the current context.
 194       *
 195       * As part of the initial restructure, the secondary nav is only considered for the following pages:
 196       * 1 - Site admin settings
 197       * 2 - Course page - Does not include front_page which has the same context.
 198       * 3 - Module page
 199       */
 200      public function initialise(): void {
 201          global $SITE;
 202  
 203          if (during_initial_install() || $this->initialised) {
 204              return;
 205          }
 206          $this->id = 'secondary_navigation';
 207          $context = $this->context;
 208          $this->headertitle = get_string('menu');
 209          $defaultmoremenunodes = [];
 210          $maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES;
 211  
 212          switch ($context->contextlevel) {
 213              case CONTEXT_COURSE:
 214                  $this->headertitle = get_string('courseheader');
 215                  if ($this->page->course->format === 'singleactivity') {
 216                      $this->load_single_activity_course_navigation();
 217                  } else {
 218                      $this->load_course_navigation();
 219                      $defaultmoremenunodes = $this->get_default_course_more_menu_nodes();
 220                  }
 221                  break;
 222              case CONTEXT_MODULE:
 223                  $this->headertitle = get_string('activityheader');
 224                  if ($this->page->course->format === 'singleactivity') {
 225                      $this->load_single_activity_course_navigation();
 226                  } else {
 227                      $this->load_module_navigation($this->page->settingsnav);
 228                      $defaultmoremenunodes = $this->get_default_module_more_menu_nodes();
 229                  }
 230                  break;
 231              case CONTEXT_COURSECAT:
 232                  $this->headertitle = get_string('categoryheader');
 233                  $this->load_category_navigation();
 234                  $defaultmoremenunodes = $this->get_default_category_more_menu_nodes();
 235                  break;
 236              case CONTEXT_SYSTEM:
 237                  $this->headertitle = get_string('homeheader');
 238                  $this->load_admin_navigation();
 239                  // If the site administration navigation was generated after load_admin_navigation().
 240                  if ($this->has_children()) {
 241                      // Do not explicitly limit the number of navigation nodes displayed in the site administration
 242                      // navigation menu.
 243                      $maxdisplayednodes = null;
 244                  }
 245                  $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();
 246                  break;
 247          }
 248  
 249          $this->remove_unwanted_nodes($this);
 250  
 251          // Don't need to show anything if only the view node is available. Remove it.
 252          if ($this->children->count() == 1) {
 253              $this->children->remove('modulepage');
 254          }
 255          // Force certain navigation nodes to be displayed in the "more" menu.
 256          $this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes);
 257          // Search and set the active node.
 258          $this->scan_for_active_node($this);
 259          $this->initialised = true;
 260      }
 261  
 262      /**
 263       * Returns a node with the action being from the first found child node that has an action (Recursive).
 264       *
 265       * @param navigation_node $node The part of the node tree we are checking.
 266       * @param navigation_node $basenode  The very first node to be used for the return.
 267       * @return navigation_node|null
 268       */
 269      protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node {
 270          $newnode = null;
 271          if (!$node->has_children()) {
 272              return null;
 273          }
 274  
 275          // Find the first child with an action and update the main node.
 276          foreach ($node->children as $child) {
 277              if ($child->has_action()) {
 278                  $newnode = $basenode;
 279                  $newnode->action = $child->action;
 280                  return $newnode;
 281              }
 282          }
 283          if (is_null($newnode)) {
 284              // Check for children and go again.
 285              foreach ($node->children as $child) {
 286                  if ($child->has_children()) {
 287                      $newnode = $this->get_node_with_first_action($child, $basenode);
 288  
 289                      if (!is_null($newnode)) {
 290                          return $newnode;
 291                      }
 292                  }
 293              }
 294          }
 295          return null;
 296      }
 297  
 298      /**
 299       * Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have
 300       * an action then a search is done through the children looking for the first node that has an action. This action is then given
 301       * to the parent node that is initially provided as a parameter.
 302       *
 303       * @param navigation_node $node The navigation node that we want to ensure has an action tied to it.
 304       * @return navigation_node The node intact with an action to use.
 305       */
 306      protected function get_first_action_for_node(navigation_node $node): ?navigation_node {
 307          // If the node does not have children and has no action then no further processing is needed.
 308          $newnode = null;
 309          if ($node->has_children() && !$node->has_action()) {
 310              // We want to find the first child with an action.
 311              // We want to check all children on this level before going further down.
 312              // Note that new node gets changed here.
 313              $newnode = $this->get_node_with_first_action($node, $node);
 314          } else if ($node->has_action()) {
 315              $newnode = $node;
 316          }
 317          return $newnode;
 318      }
 319  
 320      /**
 321       * Recursive call to add all custom navigation nodes to secondary
 322       *
 323       * @param navigation_node $node The node which should be added to secondary
 324       * @param navigation_node $basenode The original parent node
 325       * @param navigation_node|null $root The parent node nodes are to be added/removed to.
 326       * @param bool $forceadd Whether or not to bypass the external action check and force add all nodes
 327       */
 328      protected function add_external_nodes_to_secondary(navigation_node $node, navigation_node $basenode,
 329             ?navigation_node $root = null, bool $forceadd = false) {
 330          $root = $root ?? $this;
 331          // Add the first node.
 332          if ($node->has_action() && !$this->get($node->key)) {
 333              $root->add_node(clone $node);
 334          }
 335  
 336          // If the node has an external action add all children to the secondary navigation.
 337          if (!$node->has_internal_action() || $forceadd) {
 338              if ($node->has_children()) {
 339                  foreach ($node->children as $child) {
 340                      if ($child->has_children()) {
 341                          $this->add_external_nodes_to_secondary($child, $basenode, $root, true);
 342                      } else if ($child->has_action() && !$this->get($child->key)) {
 343                          // Check whether the basenode matches a child's url.
 344                          // This would have happened in get_first_action_for_node.
 345                          // In these cases, we prefer the specific child content.
 346                          if ($basenode->has_action() && $basenode->action()->compare($child->action())) {
 347                              $root->children->remove($basenode->key, $basenode->type);
 348                          }
 349                          $root->add_node(clone $child);
 350                      }
 351                  }
 352              }
 353          }
 354      }
 355  
 356      /**
 357       * Returns a list of all expected nodes in the course administration.
 358       *
 359       * @return array An array of keys for navigation nodes in the course administration.
 360       */
 361      protected function get_expected_course_admin_nodes(): array {
 362          $expectednodes = [];
 363          foreach ($this->get_default_course_mapping()['settings'] as $value) {
 364              foreach ($value as $nodekey => $notused) {
 365                  $expectednodes[] = $nodekey;
 366              }
 367          }
 368          foreach ($this->get_default_course_mapping()['navigation'] as $value) {
 369              foreach ($value as $nodekey => $notused) {
 370                  $expectednodes[] = $nodekey;
 371              }
 372          }
 373          $othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges'];
 374          $leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset'];
 375          $expectednodes = array_merge($expectednodes, $othernodes);
 376          $expectednodes = array_merge($expectednodes, $leftovercourseadminnodes);
 377          return $expectednodes;
 378      }
 379  
 380      /**
 381       * Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do
 382       * the relevant checks, we don't do it again here.
 383       *
 384       * @param navigation_node|null $rootnode The node where the course navigation nodes should be added into as children.
 385       *                                       If not explicitly defined, the nodes will be added to the secondary root
 386       *                                       node by default.
 387       */
 388      protected function load_course_navigation(?navigation_node $rootnode = null): void {
 389          global $SITE;
 390  
 391          $rootnode = $rootnode ?? $this;
 392          $course = $this->page->course;
 393          // Initialise the main navigation and settings nav.
 394          // It is important that this is done before we try anything.
 395          $settingsnav = $this->page->settingsnav;
 396          $navigation = $this->page->navigation;
 397  
 398          if ($course->id == $SITE->id) {
 399              $firstnodeidentifier = get_string('home'); // The first node in the site course nav is called 'Home'.
 400              $frontpage = $settingsnav->get('frontpage'); // The site course nodes are children of a dedicated 'frontpage' node.
 401              $settingsnav = $frontpage ?: $settingsnav;
 402              $courseadminnode = $frontpage ?: null; // Custom nodes for the site course are also children of the 'frontpage' node.
 403          } else {
 404              $firstnodeidentifier = get_string('course'); // Regular courses have a first node called 'Course'.
 405              $courseadminnode = $settingsnav->get('courseadmin'); // Custom nodes for regular courses live under 'courseadmin'.
 406          }
 407  
 408          // Add the known nodes from settings and navigation.
 409          $nodes = $this->get_default_course_mapping();
 410          $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);
 411          $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);
 412          $this->add_ordered_nodes($nodesordered, $rootnode);
 413  
 414          // Try to get any custom nodes defined by plugins, which may include containers.
 415          if ($courseadminnode) {
 416              $expectedcourseadmin = $this->get_expected_course_admin_nodes();
 417              foreach ($courseadminnode->children as $other) {
 418                  if (array_search($other->key, $expectedcourseadmin, true) === false) {
 419                      $othernode = $this->get_first_action_for_node($other);
 420                      $recursivenode = $othernode && !$rootnode->get($othernode->key) ? $othernode : $other;
 421                      // Get the first node and check whether it's been added already.
 422                      // Also check if the first node is an external link. If it is, add all children.
 423                      $this->add_external_nodes_to_secondary($recursivenode, $recursivenode, $rootnode);
 424                  }
 425              }
 426          }
 427  
 428          // Move some nodes into a 'course reuse' node.
 429          $overflownode = $this->get_course_overflow_nodes($rootnode);
 430          if (!is_null($overflownode)) {
 431              $actionnode = $this->get_first_action_for_node($overflownode);
 432              if ($actionnode) {
 433                  // All additional nodes will be available under the 'Course reuse' page.
 434                  $text = get_string('coursereuse');
 435                  $rootnode->add($text, $actionnode->action, navigation_node::TYPE_COURSE, null, 'coursereuse',
 436                      new \pix_icon('t/edit', $text));
 437              }
 438          }
 439  
 440          // Add the respective first node, provided there are other nodes included.
 441          if (!empty($nodekeys = $rootnode->children->get_key_list())) {
 442              $rootnode->add_node(
 443                  navigation_node::create($firstnodeidentifier, new \moodle_url('/course/view.php', ['id' => $course->id]),
 444                      self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys)
 445              );
 446          }
 447      }
 448  
 449      /**
 450       * Gets the overflow navigation nodes for the course administration category.
 451       *
 452       * @param navigation_node|null $rootnode The node from where the course overflow nodes should be obtained.
 453       *                                       If not explicitly defined, the nodes will be obtained from the secondary root
 454       *                                       node by default.
 455       * @return navigation_node  The course overflow nodes.
 456       */
 457      protected function get_course_overflow_nodes(?navigation_node $rootnode = null): ?navigation_node {
 458          global $SITE;
 459  
 460          $rootnode = $rootnode ?? $this;
 461          // This gets called twice on some pages, and so trying to create this navigation node twice results in no children being
 462          // present the second time this is called.
 463          if (isset($this->courseoverflownode)) {
 464              return $this->courseoverflownode;
 465          }
 466  
 467          // Start with getting the base node for the front page or the course.
 468          $node = null;
 469          if ($this->page->course->id == $SITE->id) {
 470              $node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
 471          } else {
 472              $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
 473          }
 474          $coursesettings = $node ? $node->get_children_key_list() : [];
 475          $thissettings = $rootnode->get_children_key_list();
 476          $diff = array_diff($coursesettings, $thissettings);
 477  
 478          // Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup,
 479          // grades - outcomes).
 480          $shortdiff = array_filter($diff, function($value) {
 481              return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' ||
 482                  $value == 'outcomes');
 483          });
 484  
 485          // Permissions may be in play here that ultimately will show no overflow.
 486          if (empty($shortdiff)) {
 487              return null;
 488          }
 489  
 490          $firstitem = array_shift($shortdiff);
 491          $navnode = $node->get($firstitem);
 492          foreach ($shortdiff as $key) {
 493              $courseadminnodes = $node->get($key);
 494              if ($courseadminnodes) {
 495                  if ($courseadminnodes->parent->key == $node->key) {
 496                      $navnode->add_node($courseadminnodes);
 497                  }
 498              }
 499          }
 500          $this->courseoverflownode = $navnode;
 501          return $navnode;
 502  
 503      }
 504  
 505      /**
 506       * Recursively looks for a match to the current page url.
 507       *
 508       * @param navigation_node $node The node to look through.
 509       * @return navigation_node|null The node that matches this page's url.
 510       */
 511      protected function nodes_match_current_url(navigation_node $node): ?navigation_node {
 512          $pagenode = $this->page->url;
 513          if ($node->has_action()) {
 514              // Check this node first.
 515              if ($node->action->compare($pagenode)) {
 516                  return $node;
 517              }
 518          }
 519          if ($node->has_children()) {
 520              foreach ($node->children as $child) {
 521                  $result = $this->nodes_match_current_url($child);
 522                  if ($result) {
 523                      return $result;
 524                  }
 525              }
 526          }
 527          return null;
 528      }
 529  
 530      /**
 531       * Recursively search a node and its children for a node matching the key string $key.
 532       *
 533       * @param navigation_node $node the navigation node to check.
 534       * @param string $key the key of the node to match.
 535       * @return navigation_node|null node if found, otherwise null.
 536       */
 537      protected function node_matches_key_string(navigation_node $node, string $key): ?navigation_node {
 538          if ($node->has_action()) {
 539              // Check this node first.
 540              if ($node->key == $key) {
 541                  return $node;
 542              }
 543          }
 544          if ($node->has_children()) {
 545              foreach ($node->children as $child) {
 546                  $result = $this->node_matches_key_string($child, $key);
 547                  if ($result) {
 548                      return $result;
 549                  }
 550              }
 551          }
 552          return null;
 553      }
 554  
 555      /**
 556       * Force a specific node in the 'coursereuse' course overflow to be selected, based on the provided node key.
 557       *
 558       * Normally, the selected node is determined by matching the page URL to the node URL. E.g. The page 'backup/restorefile.php'
 559       * will match the "Restore" node which has a registered URL of 'backup/restorefile.php' because the URLs match.
 560       *
 561       * This method allows a page to choose a specific node to match, which is useful in cases where the page knows its URL won't
 562       * match the node it needs to reside under. I.e. this permits several pages to 'share' the same overflow node. When the page
 563       * knows the PAGE->url won't match the node URL, the page can simply say "I want to match the 'XXX' node".
 564       *
 565       * E.g.
 566       * - The $PAGE->url is 'backup/restore.php' (this page is used during restores but isn't the main landing page for a restore)
 567       * - The 'Restore' node in the overflow has a key of 'restore' and will only match 'backup/restorefile.php' by default (the
 568       * main restore landing page).
 569       * - The backup/restore.php page calls:
 570       * $PAGE->secondarynav->set_overflow_selected_node(new moodle_url('restore');
 571       * and when the page is loaded, the 'Restore' node be presented as the selected node.
 572       *
 573       * @param string $nodekey The string key of the overflow node to match.
 574       */
 575      public function set_overflow_selected_node(string $nodekey): void {
 576          $this->overflowselected = $nodekey;
 577      }
 578  
 579      /**
 580       * Returns a url_select object with overflow navigation nodes.
 581       * This looks to see if the current page is within the course administration, or some other page that requires an overflow
 582       * select object.
 583       *
 584       * @return url_select|null The overflow menu data.
 585       */
 586      public function get_overflow_menu_data(): ?url_select {
 587  
 588          if (!$this->page->get_navigation_overflow_state()) {
 589              return null;
 590          }
 591  
 592          $issingleactivitycourse = $this->page->course->format === 'singleactivity';
 593          $rootnode = $issingleactivitycourse ? $this->find('course', self::TYPE_COURSE) : $this;
 594          $activenode = $this->find_active_node();
 595          $incourseadmin = false;
 596  
 597          if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) {
 598              // Could be in the course admin section.
 599              $courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
 600              if (!$courseadmin) {
 601                  return null;
 602              }
 603  
 604              $activenode = $courseadmin->find_active_node();
 605              if (!$activenode) {
 606                  return null;
 607              }
 608              $incourseadmin = true;
 609          }
 610  
 611          if ($activenode->key === 'coursereuse' || $incourseadmin) {
 612              $courseoverflownode = $this->get_course_overflow_nodes($rootnode);
 613              if (is_null($courseoverflownode)) {
 614                  return null;
 615              }
 616              if ($incourseadmin) {
 617                  // Validate whether the active node is part of the expected course overflow nodes.
 618                  if (($activenode->key !== $courseoverflownode->key) &&
 619                      !$courseoverflownode->find($activenode->key, $activenode->type)) {
 620                      return null;
 621                  }
 622              }
 623              $menuarray = static::create_menu_element([$courseoverflownode]);
 624              if ($activenode->key != 'coursereuse') {
 625                  $inmenu = false;
 626                  foreach ($menuarray as $key => $value) {
 627                      if ($this->page->url->out(false) == $key) {
 628                          $inmenu = true;
 629                      }
 630                  }
 631                  if (!$inmenu) {
 632                      return null;
 633                  }
 634              }
 635              // If the page has explicitly set the overflow node it would like selected, find and use that node.
 636              if ($this->overflowselected) {
 637                  $selectedoverflownode = $this->node_matches_key_string($courseoverflownode, $this->overflowselected);
 638                  $selectedoverflownodeurl = $selectedoverflownode ? $selectedoverflownode->action->out(false) : null;
 639              }
 640  
 641              $menuselect = new url_select($menuarray, $selectedoverflownodeurl ?? $this->page->url, null);
 642              $menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'sr-only']);
 643              return $menuselect;
 644          } else {
 645              return $this->get_other_overflow_menu_data($activenode);
 646          }
 647      }
 648  
 649      /**
 650       * Gets overflow menu data for third party plugin settings.
 651       *
 652       * @param navigation_node $activenode The node to gather the children for to put into the overflow menu.
 653       * @return url_select|null The overflow menu in a url_select object.
 654       */
 655      protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select {
 656          if (!$activenode->has_action()) {
 657              return null;
 658          }
 659  
 660          if (!$activenode->has_children()) {
 661              return null;
 662          }
 663  
 664          // If the setting is extending the course navigation then the page being redirected to should be in the course context.
 665          // It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages
 666          // that are redirected to, be in the course context or module context depending on which callback was used.
 667          // Third part plugins were checked to see if any existing plugins had settings in a system context and none were found.
 668          // The request of third party developers is to keep their settings within the specified context.
 669          if ($this->page->context->contextlevel != CONTEXT_COURSE
 670                  && $this->page->context->contextlevel != CONTEXT_MODULE
 671                  && $this->page->context->contextlevel != CONTEXT_COURSECAT) {
 672              return null;
 673          }
 674  
 675          // These areas have their own code to retrieve added plugin navigation nodes.
 676          if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') {
 677              return null;
 678          }
 679  
 680          $menunode = $this->page->settingsnav->find($activenode->key, null);
 681  
 682          if (!$menunode instanceof navigation_node) {
 683              return null;
 684          }
 685          // Loop through all children and try and find a match to the current url.
 686          $matchednode = $this->nodes_match_current_url($menunode);
 687          if (is_null($matchednode)) {
 688              return null;
 689          }
 690          if (!isset($menunode) || !$menunode->has_children()) {
 691              return null;
 692          }
 693          $selectdata = static::create_menu_element([$menunode], false);
 694          $urlselect = new url_select($selectdata, $matchednode->action->out(false), null);
 695          $urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'sr-only']);
 696          return $urlselect;
 697      }
 698  
 699      /**
 700       * Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via
 701       * '_extend_settings_navigation'.
 702       * It populates the tree based on the nav mockup
 703       *
 704       * If nodes change, we will have to explicitly call the callback again.
 705       *
 706       * @param settings_navigation $settingsnav The settings navigation object related to the module page
 707       * @param navigation_node|null $rootnode The node where the module navigation nodes should be added into as children.
 708       *                                       If not explicitly defined, the nodes will be added to the secondary root
 709       *                                       node by default.
 710       */
 711      protected function load_module_navigation(settings_navigation $settingsnav, ?navigation_node $rootnode = null): void {
 712          $rootnode = $rootnode ?? $this;
 713          $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING);
 714          $nodes = $this->get_default_module_mapping();
 715  
 716          if ($mainnode) {
 717              $url = new \moodle_url('/mod/' . $settingsnav->get_page()->activityname . '/view.php',
 718                  ['id' => $settingsnav->get_page()->cm->id]);
 719              $setactive = $url->compare($settingsnav->get_page()->url, URL_MATCH_BASE);
 720              $node = $rootnode->add(get_string('modulename', $settingsnav->get_page()->activityname), $url,
 721                  null, null, 'modulepage');
 722              if ($setactive) {
 723                  $node->make_active();
 724              }
 725              // Add the initial nodes.
 726              $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
 727              $this->add_ordered_nodes($nodesordered, $rootnode);
 728  
 729              // We have finished inserting the initial structure.
 730              // Populate the menu with the rest of the nodes available.
 731              $this->load_remaining_nodes($mainnode, $nodes, $rootnode);
 732          }
 733      }
 734  
 735      /**
 736       * Load the course category navigation.
 737       */
 738      protected function load_category_navigation(): void {
 739          $settingsnav = $this->page->settingsnav;
 740          $mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER);
 741          $nodes = $this->get_default_category_mapping();
 742  
 743          if ($mainnode) {
 744              $url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]);
 745              $this->add(get_string('category'), $url, self::TYPE_CONTAINER, null, 'categorymain');
 746  
 747              // Add the initial nodes.
 748              $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
 749              $this->add_ordered_nodes($nodesordered);
 750  
 751              // We have finished inserting the initial structure.
 752              // Populate the menu with the rest of the nodes available.
 753              $this->load_remaining_nodes($mainnode, $nodes);
 754          }
 755      }
 756  
 757      /**
 758       * Load the site admin navigation
 759       */
 760      protected function load_admin_navigation(): void {
 761          global $PAGE, $SITE;
 762  
 763          $settingsnav = $this->page->settingsnav;
 764          $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN);
 765          // We need to know if we are on the main site admin search page. Here the navigation between tabs are done via
 766          // anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to
 767          // exist as links with anchors appended in order to redirect back to the admin search page and the corresponding
 768          // tab. Note this value refers to being present on the page itself, before a search has been performed.
 769          $isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS);
 770          if ($node) {
 771              $siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode');
 772              if ($isadminsearchpage) {
 773                  $siteadminnode->action = false;
 774                  $siteadminnode->tab = "#link$node->key";
 775              } else {
 776                  $siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key");
 777              }
 778              foreach ($node->children as $child) {
 779                  if ($child->display && !$child->is_short_branch()) {
 780                      // Mimic the current boost behaviour and pass down anchors for the tabs.
 781                      if ($isadminsearchpage) {
 782                          $child->action = false;
 783                          $child->tab = "#link$child->key";
 784                      } else {
 785                          $child->action = new \moodle_url("/admin/search.php", [], "link$child->key");
 786                      }
 787                      $this->add_node(clone $child);
 788                  } else {
 789                      $siteadminnode->add_node(clone $child);
 790                  }
 791              }
 792          }
 793      }
 794  
 795      /**
 796       * Adds the indexed nodes to the current view or a given node. The key should indicate it's position in the tree.
 797       * Any sub nodes needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed  under #3 node.
 798       *
 799       * @param array $nodes An array of navigation nodes to be added.
 800       * @param navigation_node|null $rootnode The node where the nodes should be added into as children. If not explicitly
 801       *                                       defined, the nodes will be added to the secondary root node by default.
 802       */
 803      protected function add_ordered_nodes(array $nodes, ?navigation_node $rootnode = null): void {
 804          $rootnode = $rootnode ?? $this;
 805          ksort($nodes);
 806          foreach ($nodes as $key => $node) {
 807              // If the key is a string then we are assuming this is a nested element.
 808              if (is_string($key)) {
 809                  $parentnode = $nodes[floor($key)] ?? null;
 810                  if ($parentnode) {
 811                      $parentnode->add_node(clone $node);
 812                  }
 813              } else {
 814                  $rootnode->add_node(clone $node);
 815              }
 816          }
 817      }
 818  
 819      /**
 820       * Find the remaining nodes that need to be loaded into secondary based on the current context or a given node.
 821       *
 822       * @param navigation_node $completenode The original node that we are sourcing information from
 823       * @param array           $nodesmap The map used to populate secondary nav in the given context
 824       * @param navigation_node|null $rootnode The node where the remaining nodes should be added into as children. If not
 825       *                                       explicitly defined, the nodes will be added to the secondary root node by
 826       *                                       default.
 827       */
 828      protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap,
 829              ?navigation_node $rootnode = null): void {
 830          $flattenednodes = [];
 831          $rootnode = $rootnode ?? $this;
 832          foreach ($nodesmap as $nodecontainer) {
 833              $flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes);
 834          }
 835  
 836          $populatedkeys = $this->get_children_key_list();
 837          $existingkeys = $completenode->get_children_key_list();
 838          $leftover = array_diff($existingkeys, $populatedkeys);
 839          foreach ($leftover as $key) {
 840              if (!in_array($key, $flattenednodes, true) && $leftovernode = $completenode->get($key)) {
 841                  // Check for nodes with children and potentially no action to direct to.
 842                  if ($leftovernode->has_children()) {
 843                      $leftovernode = $this->get_first_action_for_node($leftovernode);
 844                  }
 845  
 846                  // We have found the first node with an action.
 847                  if ($leftovernode) {
 848                      $this->add_external_nodes_to_secondary($leftovernode, $leftovernode, $rootnode);
 849                  }
 850              }
 851          }
 852      }
 853  
 854      /**
 855       * Force certain secondary navigation nodes to be displayed in the "more" menu.
 856       *
 857       * @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that
 858       *                                    should be added into the "more" menu by default
 859       * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation
 860       */
 861      protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) {
 862          // Counter of the navigation nodes that are initially displayed in the secondary nav
 863          // (excludes the nodes from the "more" menu).
 864          $displayednodescount = 0;
 865          foreach ($this->children as $child) {
 866              // Skip if the navigation node has been already forced into the "more" menu.
 867              if ($child->forceintomoremenu) {
 868                  continue;
 869              }
 870              // If the navigation node is in the pre-defined list of nodes that should be added by default in the
 871              // "more" menu or the maximum limit of displayed navigation nodes has been reached (if defined).
 872              if (in_array($child->key, $defaultmoremenunodes) ||
 873                      (!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) {
 874                  // Force the node and its children into the "more" menu.
 875                  $child->set_force_into_more_menu(true);
 876                  continue;
 877              }
 878              $displayednodescount++;
 879          }
 880      }
 881  
 882      /**
 883       * Recursively remove navigation nodes that should not be displayed in the secondary navigation.
 884       *
 885       * @param navigation_node $node The starting navigation node.
 886       */
 887      protected function remove_unwanted_nodes(navigation_node $node) {
 888          foreach ($node->children as $child) {
 889              if (!$child->showinsecondarynavigation) {
 890                  $child->remove();
 891                  continue;
 892              }
 893              if (!empty($child->children)) {
 894                  $this->remove_unwanted_nodes($child);
 895              }
 896          }
 897      }
 898  
 899      /**
 900       * Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a
 901       * url_select element.
 902       *
 903       * @param navigation_node[] $navigationnodes Navigation nodes to format into a menu.
 904       * @param bool $forceheadings Whether the returned array should be forced to use headings.
 905       * @return array|null A url select element for navigating through the navigation nodes.
 906       */
 907      public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array {
 908          if (empty($navigationnodes)) {
 909              return null;
 910          }
 911  
 912          // If one item, do we put this into a url_select?
 913          if (count($navigationnodes) < 2) {
 914              // Check if there are children.
 915              $navnode = array_shift($navigationnodes);
 916              $menudata = [];
 917              if (!$navnode->has_children()) {
 918                  // Just one item.
 919                  if (!$navnode->has_action()) {
 920                      return null;
 921                  }
 922                  $menudata[$navnode->action->out(false)] = static::format_node_text($navnode);
 923              } else {
 924                  if (static::does_menu_need_headings($navnode) || $forceheadings) {
 925                      // Let's do headings.
 926                      $menudata = static::get_headings_nav_array($navnode);
 927                  } else {
 928                      // Simple flat nav.
 929                      $menudata = static::get_flat_nav_array($navnode);
 930                  }
 931              }
 932              return $menudata;
 933          } else {
 934              // We have more than one navigation node to handle. Put each node in it's own heading.
 935              $menudata = [];
 936              $titledata = [];
 937              foreach ($navigationnodes as $navigationnode) {
 938                  if ($navigationnode->has_children()) {
 939                      $menuarray = [];
 940                      // Add a heading and flatten out everything else.
 941                      if ($navigationnode->has_action()) {
 942                          $menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
 943                              static::format_node_text($navigationnode);
 944                          $menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode);
 945                      } else {
 946                          $menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode);
 947                      }
 948  
 949                      $titledata += $menuarray;
 950                  } else {
 951                      // Add with no heading.
 952                      if (!$navigationnode->has_action()) {
 953                          return null;
 954                      }
 955                      $menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
 956                  }
 957              }
 958              $menudata += [$titledata];
 959              return $menudata;
 960          }
 961      }
 962  
 963      /**
 964       * Recursively goes through the provided navigation node and returns a flat version.
 965       *
 966       * @param navigation_node $navigationnode The navigationnode.
 967       * @return array The whole tree flat.
 968       */
 969      protected static function get_whole_tree_flat(navigation_node $navigationnode): array {
 970          $nodes = [];
 971          foreach ($navigationnode->children as $child) {
 972              if ($child->has_action()) {
 973                  $nodes[$child->action->out()] = $child->text;
 974              }
 975              if ($child->has_children()) {
 976                  $childnodes = static::get_whole_tree_flat($child);
 977                  $nodes = array_merge($nodes, $childnodes);
 978              }
 979          }
 980          return $nodes;
 981      }
 982  
 983      /**
 984       * Checks to see if the provided navigation node has children and determines if we want headings for a url select element.
 985       *
 986       * @param navigation_node  $navigationnode  The navigation node we are checking.
 987       * @return bool Whether we want headings or not.
 988       */
 989      protected static function does_menu_need_headings(navigation_node $navigationnode): bool {
 990          if (!$navigationnode->has_children()) {
 991              return false;
 992          }
 993          foreach ($navigationnode->children as $child) {
 994              if ($child->has_children()) {
 995                  return true;
 996              }
 997          }
 998          return false;
 999      }
1000  
1001      /**
1002       * Takes the navigation node and returns it in a flat fashion. This is not recursive.
1003       *
1004       * @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure.
1005       * @return array The flat navigation array.
1006       */
1007      protected static function get_flat_nav_array(navigation_node $navigationnode): array {
1008          $menuarray = [];
1009          if ($navigationnode->has_action()) {
1010              $menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
1011          }
1012  
1013          foreach ($navigationnode->children as $child) {
1014              if ($child->has_action()) {
1015                  $menuarray[$child->action->out(false)] = static::format_node_text($child);
1016              }
1017          }
1018          return $menuarray;
1019      }
1020  
1021      /**
1022       * For any navigation node that we have determined needs headings we return a more tree like array structure.
1023       *
1024       * @param navigation_node $navigationnode The navigation node to use for the formatted array structure.
1025       * @return array The headings navigation array structure.
1026       */
1027      protected static function get_headings_nav_array(navigation_node $navigationnode): array {
1028          $menublock = [];
1029          // We know that this single node has headings, so grab this for the first heading.
1030          $firstheading = [];
1031          if ($navigationnode->has_action()) {
1032              $firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
1033                  static::format_node_text($navigationnode);
1034              $firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock);
1035          } else {
1036              $firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock);
1037          }
1038           return [$firstheading + $menublock];
1039      }
1040  
1041      /**
1042       * Recursively goes and gets all children nodes.
1043       *
1044       * @param navigation_node $node The node to get the children of.
1045       * @param array $menublock Used to put all child nodes in its own container.
1046       * @return array The additional child nodes.
1047       */
1048      protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array {
1049          $nodes = [];
1050          foreach ($node->children as $child) {
1051              if (!$child->has_children()) {
1052                  if (!$child->has_action()) {
1053                      continue;
1054                  }
1055                  $nodes[$child->action->out(false)] = static::format_node_text($child);
1056              } else {
1057                  $newarray = [];
1058                  if ($child->has_action()) {
1059                      $newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child);
1060                      $newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock);
1061                  } else {
1062                      $newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock);
1063                  }
1064                  $menublock += $newarray;
1065              }
1066          }
1067          return $nodes;
1068      }
1069  
1070      /**
1071       * Returns the navigation node text in a string.
1072       *
1073       * @param navigation_node $navigationnode The navigationnode to return the text string of.
1074       * @return string The navigation node text string.
1075       */
1076      protected static function format_node_text(navigation_node $navigationnode): string {
1077          return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text;
1078      }
1079  
1080      /**
1081       * Load the single activity course secondary navigation.
1082       */
1083      protected function load_single_activity_course_navigation(): void {
1084          $page = $this->page;
1085          $course = $page->course;
1086  
1087          // Create 'Course' navigation node.
1088          $coursesecondarynode = navigation_node::create(get_string('course'), null, self::TYPE_COURSE, null, 'course');
1089          $this->load_course_navigation($coursesecondarynode);
1090          // Remove the unnecessary 'Course' child node generated in load_course_navigation().
1091          $coursehomenode = $coursesecondarynode->find('coursehome', self::TYPE_COURSE);
1092          if (!empty($coursehomenode)) {
1093              $coursehomenode->remove();
1094          }
1095  
1096          // Add the 'Course' node to the secondary navigation only if this node has children nodes.
1097          if (count($coursesecondarynode->children) > 0) {
1098              $this->add_node($coursesecondarynode);
1099              // Once all the items have been added to the 'Course' secondary navigation node, set the 'showchildreninsubmenu'
1100              // property to true. This is required to force the template to output these items within a dropdown menu.
1101              $coursesecondarynode->showchildreninsubmenu = true;
1102          }
1103  
1104          // Create 'Activity' navigation node.
1105          $activitysecondarynode = navigation_node::create(get_string('activity'), null, self::TYPE_ACTIVITY, null, 'activity');
1106  
1107          // We should display the module related navigation in the course context as well. Therefore, we need to
1108          // re-initialize the page object and manually set the course module to the one that it is currently visible in
1109          // the course in order to obtain the required module settings navigation.
1110          if ($page->context instanceof \context_course) {
1111              $this->page->set_secondary_active_tab($coursesecondarynode->key);
1112              // Get the currently used module in the single activity course.
1113              $module = current(array_filter(get_course_mods($course->id), function ($module) {
1114                  return $module->visible == 1;
1115              }));
1116              // If the default module for the single course format has not been set yet, skip displaying the module
1117              // related navigation in the secondary navigation.
1118              if (!$module) {
1119                  return;
1120              }
1121              $page = new \moodle_page();
1122              $page->set_cm($module, $course);
1123              $page->set_url(new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]));
1124          }
1125  
1126          $this->load_module_navigation($page->settingsnav, $activitysecondarynode);
1127  
1128          // Add the 'Activity' node to the secondary navigation only if this node has more that one child node.
1129          if (count($activitysecondarynode->children) > 1) {
1130              // Set the 'showchildreninsubmenu' property to true to later output the the module navigation items within
1131              // a dropdown menu.
1132              $activitysecondarynode->showchildreninsubmenu = true;
1133              $this->add_node($activitysecondarynode);
1134              if ($this->context instanceof \context_module) {
1135                  $this->page->set_secondary_active_tab($activitysecondarynode->key);
1136              }
1137          } else { // Otherwise, add the 'View activity' node to the secondary navigation.
1138              $viewactivityurl = new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]);
1139              $this->add(get_string('modulename', $page->activityname), $viewactivityurl, null, null, 'modulepage');
1140              if ($this->context instanceof \context_module) {
1141                  $this->page->set_secondary_active_tab('modulepage');
1142              }
1143          }
1144      }
1145  }