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 ReflectionMethod;
  20  
  21  /**
  22   * Primary navigation renderable test
  23   *
  24   * @package     core
  25   * @category    navigation
  26   * @copyright   2021 onwards Peter Dias
  27   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  class primary_test extends \advanced_testcase {
  30      /**
  31       * Basic setup to make sure the nav objects gets generated without any issues.
  32       */
  33      public function setUp(): void {
  34          global $PAGE;
  35          $this->resetAfterTest();
  36          $pagecourse = $this->getDataGenerator()->create_course();
  37          $assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]);
  38          $cm = get_coursemodule_from_id('assign', $assign->cmid);
  39          $contextrecord = \context_module::instance($cm->id);
  40          $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->instance]);
  41          $PAGE->set_cm($cm);
  42          $PAGE->set_url($pageurl);
  43          $PAGE->set_course($pagecourse);
  44          $PAGE->set_context($contextrecord);
  45      }
  46  
  47      /**
  48       * Test the primary export to confirm we are getting the nodes
  49       *
  50       * @dataProvider primary_export_provider
  51       * @param bool $withcustom Setup with custom menu
  52       * @param bool $withlang Setup with langs
  53       * @param string $userloggedin The type of user ('admin' or 'guest') if creating setup with logged in user,
  54       *                             otherwise consider the user as non-logged in
  55       * @param array $expecteditems An array of nodes expected with content in them.
  56       */
  57      public function test_primary_export(bool $withcustom, bool $withlang, string $userloggedin, array $expecteditems) {
  58          global $PAGE, $CFG;
  59          if ($withcustom) {
  60              $CFG->custommenuitems = "Course search|/course/search.php
  61                  Google|https://google.com.au/
  62                  Netflix|https://netflix.com/au";
  63          }
  64          if ($userloggedin === 'admin') {
  65              $this->setAdminUser();
  66          } else if ($userloggedin === 'guest') {
  67              $this->setGuestUser();
  68          } else {
  69              $this->setUser(0);
  70          }
  71  
  72          // Mimic multiple langs installed. To trigger responses 'get_list_of_translations'.
  73          // Note: The text/title of the nodes generated will be 'English(fr), English(de)' but we don't care about this.
  74          // We are testing whether the nodes gets generated when the lang menu is available.
  75          if ($withlang) {
  76              mkdir("$CFG->dataroot/lang/de", 0777, true);
  77              mkdir("$CFG->dataroot/lang/fr", 0777, true);
  78              // Ensure the new langs are picked up and not taken from the cache.
  79              $stringmanager = get_string_manager();
  80              $stringmanager->reset_caches(true);
  81          }
  82  
  83          $primary = new primary($PAGE);
  84          $renderer = $PAGE->get_renderer('core');
  85          $data = array_filter($primary->export_for_template($renderer));
  86  
  87          // Assert that the number of returned menu items equals the expected result.
  88          $this->assertCount(count($expecteditems), $data);
  89          // Assert that returned menu items match the expected items.
  90          foreach ($data as $menutype => $value) {
  91              $this->assertTrue(in_array($menutype, $expecteditems));
  92          }
  93          // When the user is logged in (excluding guest access), assert that lang menu is included as a part of the
  94          // user menu when multiple languages are installed.
  95          if (isloggedin() && !isguestuser()) {
  96              // Look for a language menu item within the user menu items.
  97              $usermenulang = array_filter($data['user']['items'], function($usermenuitem) {
  98                  return $usermenuitem->itemtype !== 'divider' && $usermenuitem->title === get_string('language');
  99              });
 100              if ($withlang) { // If multiple languages are installed.
 101                  // Assert that the language menu exists within the user menu.
 102                  $this->assertNotEmpty($usermenulang);
 103              } else { // If the aren't any additional installed languages.
 104                  $this->assertEmpty($usermenulang);
 105              }
 106          } else { // Otherwise assert that the user menu does not contain any items.
 107              $this->assertArrayNotHasKey('items', $data['user']);
 108          }
 109      }
 110  
 111      /**
 112       * Provider for the test_primary_export function.
 113       *
 114       * @return array
 115       */
 116      public function primary_export_provider(): array {
 117          return [
 118              "Export the menu data when: custom menu exists; multiple langs installed; user is not logged in." => [
 119                  true, true, '', ['mobileprimarynav', 'moremenu', 'lang', 'user']
 120              ],
 121              "Export the menu data when: custom menu exists; langs not installed; user is not logged in." => [
 122                  true, false, '', ['mobileprimarynav', 'moremenu', 'user']
 123              ],
 124              "Export the menu data when: custom menu exists; multiple langs installed; logged in as admin." => [
 125                  true, true, 'admin', ['mobileprimarynav', 'moremenu', 'user']
 126              ],
 127              "Export the menu data when: custom menu exists; langs not installed; logged in as admin." => [
 128                  true, false, 'admin', ['mobileprimarynav', 'moremenu', 'user']
 129              ],
 130              "Export the menu data when: custom menu exists; multiple langs installed; logged in as guest." => [
 131                  true, true, 'guest', ['mobileprimarynav', 'moremenu', 'lang', 'user']
 132              ],
 133              "Export the menu data when: custom menu exists; langs not installed; logged in as guest." => [
 134                  true, false, 'guest', ['mobileprimarynav', 'moremenu', 'user']
 135              ],
 136              "Export the menu data when: custom menu does not exist; multiple langs installed; logged in as guest." => [
 137                  false, true, 'guest', ['mobileprimarynav', 'moremenu', 'lang', 'user']
 138              ],
 139              "Export the menu data when: custom menu does not exist; multiple langs installed; logged in as admin." => [
 140                  false, true, 'admin', ['mobileprimarynav', 'moremenu', 'user']
 141              ],
 142              "Export the menu data when: custom menu does not exist; langs not installed; user is not logged in." => [
 143                  false, false, '', ['mobileprimarynav', 'moremenu', 'user']
 144              ],
 145          ];
 146      }
 147  
 148      /**
 149       * Test the custom menu getter to confirm the nodes gets generated and are returned correctly.
 150       *
 151       * @dataProvider custom_menu_provider
 152       * @param string $config
 153       * @param array $expected
 154       */
 155      public function test_get_custom_menu(string $config, array $expected) {
 156          $actual = $this->get_custom_menu($config);
 157          $this->assertEquals($expected, $actual);
 158      }
 159  
 160      /**
 161       * Helper method to get the template data for the custommenuitem that is set here via parameter.
 162       * @param string $config
 163       * @return array
 164       * @throws \ReflectionException
 165       */
 166      protected function get_custom_menu(string $config): array {
 167          global $CFG, $PAGE;
 168          $CFG->custommenuitems = $config;
 169          $output = new primary($PAGE);
 170          $method = new ReflectionMethod('core\navigation\output\primary', 'get_custom_menu');
 171          $method->setAccessible(true);
 172          $renderer = $PAGE->get_renderer('core');
 173  
 174          // We can't assert the value of each menuitem "moremenuid" property (because it's random).
 175          $custommenufilter = static function(array $custommenu) use (&$custommenufilter): void {
 176              foreach ($custommenu as $menuitem) {
 177                  unset($menuitem->moremenuid);
 178                  // Recursively move through child items.
 179                  $custommenufilter($menuitem->children);
 180              }
 181          };
 182  
 183          $actual = $method->invoke($output, $renderer);
 184          $custommenufilter($actual);
 185          return $actual;
 186      }
 187  
 188      /**
 189       * Provider for test_get_custom_menu
 190       *
 191       * @return array
 192       */
 193      public function custom_menu_provider(): array {
 194          return [
 195              'Simple custom menu' => [
 196                  "Course search|/course/search.php
 197                  Google|https://google.com.au/
 198                  Netflix|https://netflix.com/au", [
 199                      (object) [
 200                          'text' => 'Course search',
 201                          'url' => 'https://www.example.com/moodle/course/search.php',
 202                          'title' => '',
 203                          'sort' => 1,
 204                          'children' => [],
 205                          'haschildren' => false,
 206                      ],
 207                      (object) [
 208                          'text' => 'Google',
 209                          'url' => 'https://google.com.au/',
 210                          'title' => '',
 211                          'sort' => 2,
 212                          'children' => [],
 213                          'haschildren' => false,
 214                      ],
 215                      (object) [
 216                          'text' => 'Netflix',
 217                          'url' => 'https://netflix.com/au',
 218                          'title' => '',
 219                          'sort' => 3,
 220                          'children' => [],
 221                          'haschildren' => false,
 222                      ],
 223                  ]
 224              ],
 225              'Complex, nested custom menu' => [
 226                  "Moodle community|http://moodle.org
 227                  -Moodle free support|http://moodle.org/support
 228                  -Moodle development|http://moodle.org/development
 229                  --Moodle Tracker|http://tracker.moodle.org
 230                  --Moodle Docs|https://docs.moodle.org
 231                  -Moodle News|http://moodle.org/news
 232                  Moodle company
 233                  -Moodle commercial hosting|http://moodle.com/hosting
 234                  -Moodle commercial support|http://moodle.com/support", [
 235                      (object) [
 236                          'text' => 'Moodle community',
 237                          'url' => 'http://moodle.org',
 238                          'title' => '',
 239                          'sort' => 1,
 240                          'children' => [
 241                              (object) [
 242                                  'text' => 'Moodle free support',
 243                                  'url' => 'http://moodle.org/support',
 244                                  'title' => '',
 245                                  'sort' => 2,
 246                                  'children' => [],
 247                                  'haschildren' => false,
 248                              ],
 249                              (object) [
 250                                  'text' => 'Moodle development',
 251                                  'url' => 'http://moodle.org/development',
 252                                  'title' => '',
 253                                  'sort' => 3,
 254                                  'children' => [
 255                                      (object) [
 256                                          'text' => 'Moodle Tracker',
 257                                          'url' => 'http://tracker.moodle.org',
 258                                          'title' => '',
 259                                          'sort' => 4,
 260                                          'children' => [],
 261                                          'haschildren' => false,
 262                                      ],
 263                                      (object) [
 264                                          'text' => 'Moodle Docs',
 265                                          'url' => 'https://docs.moodle.org',
 266                                          'title' => '',
 267                                          'sort' => 5,
 268                                          'children' => [],
 269                                          'haschildren' => false,
 270                                      ],
 271                                  ],
 272                                  'haschildren' => true,
 273                              ],
 274                              (object) [
 275                                  'text' => 'Moodle News',
 276                                  'url' => 'http://moodle.org/news',
 277                                  'title' => '',
 278                                  'sort' => 6,
 279                                  'children' => [],
 280                                  'haschildren' => false,
 281                              ],
 282                          ],
 283                          'haschildren' => true,
 284                      ],
 285                      (object) [
 286                          'text' => 'Moodle company',
 287                          'url' => null,
 288                          'title' => '',
 289                          'sort' => 7,
 290                          'children' => [
 291                              (object) [
 292                                  'text' => 'Moodle commercial hosting',
 293                                  'url' => 'http://moodle.com/hosting',
 294                                  'title' => '',
 295                                  'sort' => 8,
 296                                  'children' => [],
 297                                  'haschildren' => false,
 298                              ],
 299                              (object) [
 300                                  'text' => 'Moodle commercial support',
 301                                  'url' => 'http://moodle.com/support',
 302                                  'title' => '',
 303                                  'sort' => 9,
 304                                  'children' => [],
 305                                  'haschildren' => false,
 306                              ],
 307                          ],
 308                          'haschildren' => true,
 309                      ],
 310                  ]
 311              ]
 312          ];
 313      }
 314  
 315      /**
 316       * Test the merge_primary_and_custom and the eval_is_active method. Merge  primary and custom menu with different
 317       * page urls and check that the correct nodes are active and open, depending on the data for each menu.
 318       *
 319       * @covers \core\navigation\output\primary::merge_primary_and_custom
 320       * @covers \core\navigation\output\primary::flag_active_nodes
 321       * @return void
 322       * @throws \ReflectionException
 323       * @throws \moodle_exception
 324       */
 325      public function test_merge_primary_and_custom() {
 326          global $PAGE;
 327  
 328          $menu = $this->merge_and_render_menus();
 329  
 330          $this->assertEquals(4, count(\array_keys($menu)));
 331          $msg = 'No active nodes for page ' . $PAGE->url;
 332          $this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isactive'), $msg);
 333          $this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isopen'), str_replace('active', 'open', $msg));
 334  
 335          $msg = 'Active nodes desktop for /course/search.php';
 336          $menu = $this->merge_and_render_menus('/course/search.php');
 337          $isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
 338          $this->assertEquals(['Courses', 'Course search'], $isactive, $msg);
 339          $this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isopem'), str_replace('Active', 'Open', $msg));
 340  
 341          $msg = 'Active nodes mobile for /course/search.php';
 342          $menu = $this->merge_and_render_menus('/course/search.php', true);
 343          $isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
 344          $this->assertEquals(['Course search'], $isactive, $msg);
 345          $isopen = $this->get_menu_item_names_by_type($menu, 'isopen');
 346          $this->assertEquals(['Courses'], $isopen, str_replace('Active', 'Open', $msg));
 347  
 348          $msg = 'Active nodes desktop for /course/search.php?areaids=core_course-course&q=test';
 349          $menu = $this->merge_and_render_menus('/course/search.php?areaids=core_course-course&q=test');
 350          $isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
 351          $this->assertEquals(['Courses', 'Course search'], $isactive, $msg);
 352  
 353          $msg = 'Active nodes desktop for /?theme=boost';
 354          $menu = $this->merge_and_render_menus('/?theme=boost');
 355          $isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
 356          $this->assertEquals(['Theme', 'Boost'], $isactive, $msg);
 357      }
 358  
 359      /**
 360       * Internal function to get an array of top menu items from the primary and the custom menu. The latter is defined
 361       * in this function.
 362       * @param string|null $url
 363       * @param bool|null $ismobile
 364       * @return array
 365       * @throws \ReflectionException
 366       * @throws \coding_exception
 367       */
 368      protected function merge_and_render_menus(?string $url = null, ?bool $ismobile = false): array {
 369          global $PAGE, $FULLME;
 370  
 371          if ($url !== null) {
 372              $PAGE->set_url($url);
 373              $FULLME = $PAGE->url->out();
 374          }
 375          $primary = new primary($PAGE);
 376  
 377          $method = new ReflectionMethod('core\navigation\output\primary', 'get_primary_nav');
 378          $method->setAccessible(true);
 379          $dataprimary = $method->invoke($primary);
 380  
 381          // Take this custom menu that would come from the  setting custommenitems.
 382          $custommenuitems = <<< ENDMENU
 383          Theme
 384          -Boost|/?theme=boost
 385          -Classic|/?theme=classic
 386          -Purge Cache|/admin/purgecaches.php
 387          Courses
 388          -All courses|/course/
 389          -Course search|/course/search.php
 390          -###
 391          -FAQ|https://example.org/faq
 392          -My Important Course|/course/view.php?id=4
 393          Mobile app|https://example.org/app|Download our app
 394          ENDMENU;
 395  
 396          $datacustom = $this->get_custom_menu($custommenuitems);
 397          $method = new ReflectionMethod('core\navigation\output\primary', 'merge_primary_and_custom');
 398          $method->setAccessible(true);
 399          $menucomplete = $method->invoke($primary, $dataprimary, $datacustom, $ismobile);
 400          return $menucomplete;
 401      }
 402  
 403      /**
 404       * Traverse the menu array structure (all nodes recursively) and fetch the node texts from the menu nodes that are
 405       * active/open (determined via param $nodetype that can be "inactive" or "isopen"). The returned array contains a
 406       * list of nade names that match this criterion.
 407       * @param array $menu
 408       * @param string $nodetype
 409       * @return array
 410       */
 411      protected function get_menu_item_names_by_type(array $menu, string $nodetype): array {
 412          $matchednodes = [];
 413          foreach ($menu as $menuitem) {
 414              // Either the node is an array.
 415              if (is_array($menuitem)) {
 416                  if ($menuitem[$nodetype] ?? false) {
 417                      $matchednodes[] = $menuitem['text'];
 418                  }
 419                  // Recursively move through child items.
 420                  if (array_key_exists('children', $menuitem) && count($menuitem['children'])) {
 421                      $matchednodes = array_merge($matchednodes, $this->get_menu_item_names_by_type($menuitem['children'], $nodetype));
 422                  }
 423              } else {
 424                  // Otherwise the node is a standard object.
 425                  if (isset($menuitem->{$nodetype}) && $menuitem->{$nodetype} === true) {
 426                      $matchednodes[] = $menuitem->text;
 427                  }
 428                  // Recursively move through child items.
 429                  if (isset($menuitem->children) && is_array($menuitem->children) && !empty($menuitem->children)) {
 430                      $matchednodes = array_merge($matchednodes, $this->get_menu_item_names_by_type($menuitem->children, $nodetype));
 431                  }
 432              }
 433          }
 434          return $matchednodes;
 435      }
 436  }