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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body