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\output;
  18  
  19  use renderable;
  20  use renderer_base;
  21  use templatable;
  22  use custom_menu;
  23  
  24  /**
  25   * Primary navigation renderable
  26   *
  27   * This file combines primary nav, custom menu, lang menu and
  28   * usermenu into a standardized format for the frontend
  29   *
  30   * @package     core
  31   * @category    navigation
  32   * @copyright   2021 onwards Peter Dias
  33   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class primary implements renderable, templatable {
  36      /** @var \moodle_page $page the moodle page that the navigation belongs to */
  37      private $page = null;
  38  
  39      /**
  40       * primary constructor.
  41       * @param \moodle_page $page
  42       */
  43      public function __construct($page) {
  44          $this->page = $page;
  45      }
  46  
  47      /**
  48       * Combine the various menus into a standardized output.
  49       *
  50       * @param renderer_base|null $output
  51       * @return array
  52       */
  53      public function export_for_template(?renderer_base $output = null): array {
  54          if (!$output) {
  55              $output = $this->page->get_renderer('core');
  56          }
  57  
  58          $menudata = (object) $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output));
  59          $moremenu = new \core\navigation\output\more_menu($menudata, 'navbar-nav', false);
  60          $mobileprimarynav = $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output), true);
  61  
  62          $languagemenu = new \core\output\language_menu($this->page);
  63  
  64          return [
  65              'mobileprimarynav' => $mobileprimarynav,
  66              'moremenu' => $moremenu->export_for_template($output),
  67              'lang' => !isloggedin() || isguestuser() ? $languagemenu->export_for_template($output) : [],
  68              'user' => $this->get_user_menu($output),
  69          ];
  70      }
  71  
  72      /**
  73       * Get the primary nav object and standardize the output
  74       *
  75       * @param \navigation_node|null $parent used for nested nodes, by default the primarynav node
  76       * @return array
  77       */
  78      protected function get_primary_nav($parent = null): array {
  79          if ($parent === null) {
  80              $parent = $this->page->primarynav;
  81          }
  82          $nodes = [];
  83          foreach ($parent->children as $node) {
  84              $children = $this->get_primary_nav($node);
  85              $activechildren = array_filter($children, function($child) {
  86                  return !empty($child['isactive']);
  87              });
  88              if ($node->preceedwithhr && count($nodes) && empty($nodes[count($nodes) - 1]['divider'])) {
  89                  $nodes[] = ['divider' => true];
  90              }
  91              $nodes[] = [
  92                  'title' => $node->get_title(),
  93                  'url' => $node->action(),
  94                  'text' => $node->text,
  95                  'icon' => $node->icon,
  96                  'isactive' => $node->isactive || !empty($activechildren),
  97                  'key' => $node->key,
  98                  'children' => $children,
  99                  'haschildren' => !empty($children) ? 1 : 0,
 100              ];
 101          }
 102  
 103          return $nodes;
 104      }
 105  
 106      /**
 107       * Custom menu items reside on the same level as the original nodes.
 108       * Fetch and convert the nodes to a standardised array.
 109       *
 110       * @param renderer_base $output
 111       * @return array
 112       */
 113      protected function get_custom_menu(renderer_base $output): array {
 114          global $CFG;
 115  
 116          // Early return if a custom menu does not exists.
 117          if (empty($CFG->custommenuitems)) {
 118              return [];
 119          }
 120  
 121          $custommenuitems = $CFG->custommenuitems;
 122          $currentlang = current_language();
 123          $custommenunodes = custom_menu::convert_text_to_menu_nodes($custommenuitems, $currentlang);
 124          $nodes = [];
 125          foreach ($custommenunodes as $node) {
 126              $nodes[] = $node->export_for_template($output);
 127          }
 128  
 129          return $nodes;
 130      }
 131  
 132      /**
 133       * When defining custom menu items, the active flag is not obvserved correctly. Therefore, the merge of the primary
 134       * and custom navigation must be handled a bit smarter. Change the "isactive" flag of the nodes (this may set by
 135       * default in the primary nav nodes but is entirely missing in the custom nav nodes).
 136       * Set the $expandedmenu argument to true when the menu for the mobile template is build.
 137       *
 138       * @param array $primary
 139       * @param array $custom
 140       * @param bool $expandedmenu
 141       * @return array
 142       */
 143      protected function merge_primary_and_custom(array $primary, array $custom, bool $expandedmenu = false): array {
 144          if (empty($custom)) {
 145              return $primary; // No custom nav, nothing to merge.
 146          }
 147          // Remember the amount of primary nodes and whether we changed the active flag in the custom menu nodes.
 148          $primarylen = count($primary);
 149          $changed = false;
 150          foreach (array_keys($custom) as $i) {
 151              if (!$changed) {
 152                  if ($this->flag_active_nodes($custom[$i], $expandedmenu)) {
 153                      $changed = true;
 154                  }
 155              }
 156              $primary[] = $custom[$i];
 157          }
 158          // In case some custom node is active, mark all primary nav elements as inactive.
 159          if ($changed) {
 160              for ($i = 0; $i < $primarylen; $i++) {
 161                  $primary[$i]['isactive'] = false;
 162              }
 163          }
 164          return $primary;
 165      }
 166  
 167      /**
 168       * Recursive checks if any of the children is active. If that's the case this node (the parent) is active as
 169       * well. If the node has no children, check if the node itself is active. Use pass by reference for the node
 170       * object because we actively change/set the "isactive" flag inside the method and this needs to be kept at the
 171       * callers side.
 172       * Set $expandedmenu to true, if the mobile menu is done, in this case the active flag gets the node that is
 173       * actually active, while the parent hierarchy of the active node gets the flag isopen.
 174       *
 175       * @param object $node
 176       * @param bool $expandedmenu
 177       * @return bool
 178       */
 179      protected function flag_active_nodes(object $node, bool $expandedmenu = false): bool {
 180          global $FULLME;
 181          $active = false;
 182          foreach (array_keys($node->children ?? []) as $c) {
 183              if ($this->flag_active_nodes($node->children[$c], $expandedmenu)) {
 184                  $active = true;
 185              }
 186          }
 187          // One of the children is active, so this node (the parent) is active as well.
 188          if ($active) {
 189              if ($expandedmenu) {
 190                  $node->isopen = true;
 191              } else {
 192                  $node->isactive = true;
 193              }
 194              return true;
 195          }
 196  
 197          // By default, the menu item node to check is not active.
 198          $node->isactive = false;
 199  
 200          // Check if the node url matches the called url. The node url may omit the trailing index.php, therefore check
 201          // this as well.
 202          if (empty($node->url)) {
 203              // Current menu node has no url set, so it can't be active.
 204              return false;
 205          }
 206          $nodeurl = parse_url($node->url);
 207          $current = parse_url($FULLME ?? '');
 208  
 209          $pathmatches = false;
 210  
 211          // Exact match of the path of node and current url.
 212          $nodepath = $nodeurl['path'] ?? '/';
 213          $currentpath = $current['path'] ?? '/';
 214          if ($nodepath === $currentpath) {
 215              $pathmatches = true;
 216          }
 217          // The current url may be trailed by a index.php, otherwise it's the same as the node path.
 218          if (!$pathmatches && $nodepath . 'index.php' === $currentpath) {
 219              $pathmatches = true;
 220          }
 221          // No path did match, so the node can't be active.
 222          if (!$pathmatches) {
 223              return false;
 224          }
 225          // We are here because the path matches, so now look at the query string.
 226          $nodequery = $nodeurl['query'] ?? '';
 227          $currentquery = $current['query'] ?? '';
 228          // If the node has no query string defined, then the patch match is sufficient.
 229          if (empty($nodeurl['query'])) {
 230              $node->isactive = true;
 231              return true;
 232          }
 233          // If the node contains a query string then also the current url must match this query.
 234          if ($nodequery === $currentquery) {
 235              $node->isactive = true;
 236          }
 237          return $node->isactive;
 238      }
 239  
 240      /**
 241       * Get/Generate the user menu.
 242       *
 243       * This is leveraging the data from user_get_user_navigation_info and the logic in $OUTPUT->user_menu()
 244       *
 245       * @param renderer_base $output
 246       * @return array
 247       */
 248      public function get_user_menu(renderer_base $output): array {
 249          global $CFG, $USER, $PAGE;
 250          require_once($CFG->dirroot . '/user/lib.php');
 251  
 252          $usermenudata = [];
 253          $submenusdata = [];
 254          $info = user_get_user_navigation_info($USER, $PAGE);
 255          if (isset($info->unauthenticateduser)) {
 256              $info->unauthenticateduser['content'] = get_string($info->unauthenticateduser['content']);
 257              $info->unauthenticateduser['url'] = get_login_url();
 258              return (array) $info;
 259          }
 260          // Gather all the avatar data to be displayed in the user menu.
 261          $usermenudata['avatardata'][] = [
 262              'content' => $info->metadata['useravatar'],
 263              'classes' => 'current'
 264          ];
 265          $usermenudata['userfullname'] = $info->metadata['realuserfullname'] ?? $info->metadata['userfullname'];
 266  
 267          // Logged in as someone else.
 268          if ($info->metadata['asotheruser']) {
 269              $usermenudata['avatardata'][] = [
 270                  'content' => $info->metadata['realuseravatar'],
 271                  'classes' => 'realuser'
 272              ];
 273              $usermenudata['metadata'][] = [
 274                  'content' => get_string('loggedinas', 'moodle', $info->metadata['userfullname']),
 275                  'classes' => 'viewingas'
 276              ];
 277          }
 278  
 279          // Gather all the meta data to be displayed in the user menu.
 280          $metadata = [
 281              'asotherrole' => [
 282                  'value' => 'rolename',
 283                  'class' => 'role role-##GENERATEDCLASS##',
 284              ],
 285              'userloginfail' => [
 286                  'value' => 'userloginfail',
 287                  'class' => 'loginfailures',
 288              ],
 289              'asmnetuser' => [
 290                  'value' => 'mnetidprovidername',
 291                  'class' => 'mnet mnet-##GENERATEDCLASS##',
 292              ],
 293          ];
 294          foreach ($metadata as $key => $value) {
 295              if (!empty($info->metadata[$key])) {
 296                  $content = $info->metadata[$value['value']] ?? '';
 297                  $generatedclass = strtolower(preg_replace('#[ ]+#', '-', trim($content)));
 298                  $customclass = str_replace('##GENERATEDCLASS##', $generatedclass, ($value['class'] ?? ''));
 299                  $usermenudata['metadata'][] = [
 300                      'content' => $content,
 301                      'classes' => $customclass
 302                  ];
 303              }
 304          }
 305  
 306          $modifiedarray = array_map(function($value) {
 307              $value->divider = $value->itemtype == 'divider';
 308              $value->link = $value->itemtype == 'link';
 309              if (isset($value->pix) && !empty($value->pix)) {
 310                  $value->pixicon = $value->pix;
 311                  unset($value->pix);
 312              }
 313              return $value;
 314          }, $info->navitems);
 315  
 316          // Include the language menu as a submenu within the user menu.
 317          $languagemenu = new \core\output\language_menu($this->page);
 318          $langmenu = $languagemenu->export_for_template($output);
 319          if (!empty($langmenu)) {
 320              $languageitems = $langmenu['items'];
 321              // If there are available languages, generate the data for the the language selector submenu.
 322              if (!empty($languageitems)) {
 323                  $langsubmenuid = uniqid();
 324                  // Generate the data for the link to language selector submenu.
 325                  $language = (object) [
 326                      'itemtype' => 'submenu-link',
 327                      'submenuid' => $langsubmenuid,
 328                      'title' => get_string('language'),
 329                      'divider' => false,
 330                      'submenulink' => true,
 331                  ];
 332  
 333                  // Place the link before the 'Log out' menu item which is either the last item in the menu or
 334                  // second to last when 'Switch roles' is available.
 335                  $menuposition = count($modifiedarray) - 1;
 336                  if (has_capability('moodle/role:switchroles', $PAGE->context)) {
 337                      $menuposition = count($modifiedarray) - 2;
 338                  }
 339                  array_splice($modifiedarray, $menuposition, 0, [$language]);
 340  
 341                  // Generate the data for the language selector submenu.
 342                  $submenusdata[] = (object)[
 343                      'id' => $langsubmenuid,
 344                      'title' => get_string('languageselector'),
 345                      'items' => $languageitems,
 346                  ];
 347              }
 348          }
 349  
 350          // Add divider before the last item.
 351          $modifiedarray[count($modifiedarray) - 2]->divider = true;
 352          $usermenudata['items'] = $modifiedarray;
 353          $usermenudata['submenus'] = array_values($submenusdata);
 354  
 355          return $usermenudata;
 356      }
 357  }