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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [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  /**
  18   * core_component related tests.
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2013 Petr Skoda {@link http://skodak.org}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   *
  25   * @covers \core_component
  26   */
  27  class component_test extends advanced_testcase {
  28  
  29      /**
  30       * To be changed if number of subsystems increases/decreases,
  31       * this is defined here to annoy devs that try to add more without any thinking,
  32       * always verify that it does not collide with any existing add-on modules and subplugins!!!
  33       */
  34      const SUBSYSTEMCOUNT = 77;
  35  
  36      public function test_get_core_subsystems() {
  37          global $CFG;
  38  
  39          $subsystems = core_component::get_core_subsystems();
  40  
  41          $this->assertCount(self::SUBSYSTEMCOUNT, $subsystems, 'Oh, somebody added or removed a core subsystem, think twice before doing that!');
  42  
  43          // Make sure all paths are full/null, exist and are inside dirroot.
  44          foreach ($subsystems as $subsystem => $fulldir) {
  45              $this->assertFalse(strpos($subsystem, '_'), 'Core subsystems must be one work without underscores');
  46              if ($fulldir === null) {
  47                  if ($subsystem === 'filepicker' or $subsystem === 'help') {
  48                      // Arrgghh, let's not introduce more subsystems for no real reason...
  49                  } else {
  50                      // Lang strings.
  51                      $this->assertFileExists("$CFG->dirroot/lang/en/$subsystem.php", 'Core subsystems without fulldir are usually used for lang strings.');
  52                  }
  53                  continue;
  54              }
  55              $this->assertFileExists($fulldir);
  56              // Check that base uses realpath() separators and "/" in the subdirs.
  57              $this->assertStringStartsWith($CFG->dirroot.'/', $fulldir);
  58              $reldir = substr($fulldir, strlen($CFG->dirroot)+1);
  59              $this->assertFalse(strpos($reldir, '\\'));
  60          }
  61  
  62          // Make sure all core language files are also subsystems!
  63          $items = new DirectoryIterator("$CFG->dirroot/lang/en");
  64          foreach ($items as $item) {
  65              if ($item->isDot() or $item->isDir()) {
  66                  continue;
  67              }
  68              $file = $item->getFilename();
  69              if ($file === 'moodle.php') {
  70                  // Do not add new lang strings unless really necessary!!!
  71                  continue;
  72              }
  73  
  74              if (substr($file, -4) !== '.php') {
  75                  continue;
  76              }
  77              $file = substr($file, 0, strlen($file)-4);
  78              $this->assertArrayHasKey($file, $subsystems, 'All core lang files should be subsystems, think twice before adding anything!');
  79          }
  80          unset($item);
  81          unset($items);
  82  
  83      }
  84  
  85      public function test_deprecated_get_core_subsystems() {
  86          global $CFG;
  87  
  88          $subsystems = core_component::get_core_subsystems();
  89  
  90          $this->assertSame($subsystems, get_core_subsystems(true));
  91  
  92          $realsubsystems = get_core_subsystems();
  93          $this->assertDebuggingCalled();
  94          $this->assertSame($realsubsystems, get_core_subsystems(false));
  95          $this->assertDebuggingCalled();
  96  
  97          $this->assertEquals(count($subsystems), count($realsubsystems));
  98  
  99          foreach ($subsystems as $subsystem => $fulldir) {
 100              $this->assertArrayHasKey($subsystem, $realsubsystems);
 101              if ($fulldir === null) {
 102                  $this->assertNull($realsubsystems[$subsystem]);
 103                  continue;
 104              }
 105              $this->assertSame($fulldir, $CFG->dirroot.'/'.$realsubsystems[$subsystem]);
 106          }
 107      }
 108  
 109      public function test_get_plugin_types() {
 110          global $CFG;
 111  
 112          $this->assertTrue(empty($CFG->themedir), 'Non-empty $CFG->themedir is not covered by any tests yet, you need to disable it.');
 113  
 114          $plugintypes = core_component::get_plugin_types();
 115  
 116          foreach ($plugintypes as $plugintype => $fulldir) {
 117              $this->assertStringStartsWith("$CFG->dirroot/", $fulldir);
 118          }
 119      }
 120  
 121      public function test_deprecated_get_plugin_types() {
 122          global $CFG;
 123  
 124          $plugintypes = core_component::get_plugin_types();
 125  
 126          $this->assertSame($plugintypes, get_plugin_types());
 127          $this->assertSame($plugintypes, get_plugin_types(true));
 128  
 129          $realplugintypes = get_plugin_types(false);
 130          $this->assertDebuggingCalled();
 131  
 132          foreach ($plugintypes as $plugintype => $fulldir) {
 133              $this->assertSame($fulldir, $CFG->dirroot.'/'.$realplugintypes[$plugintype]);
 134          }
 135      }
 136  
 137      public function test_get_plugin_list() {
 138          global $CFG;
 139  
 140          $plugintypes = core_component::get_plugin_types();
 141  
 142          foreach ($plugintypes as $plugintype => $fulldir) {
 143              $plugins = core_component::get_plugin_list($plugintype);
 144              foreach ($plugins as $pluginname => $plugindir) {
 145                  $this->assertStringStartsWith("$CFG->dirroot/", $plugindir);
 146              }
 147              if ($plugintype !== 'auth') {
 148                  // Let's crosscheck it with independent implementation (auth/db is an exception).
 149                  $reldir = substr($fulldir, strlen($CFG->dirroot)+1);
 150                  $dirs = get_list_of_plugins($reldir);
 151                  $dirs = array_values($dirs);
 152                  $this->assertDebuggingCalled();
 153                  $this->assertSame($dirs, array_keys($plugins));
 154              }
 155          }
 156      }
 157  
 158      public function test_deprecated_get_plugin_list() {
 159          $plugintypes = core_component::get_plugin_types();
 160  
 161          foreach ($plugintypes as $plugintype => $fulldir) {
 162              $plugins = core_component::get_plugin_list($plugintype);
 163              $this->assertSame($plugins, get_plugin_list($plugintype));
 164          }
 165      }
 166  
 167      public function test_get_plugin_directory() {
 168          $plugintypes = core_component::get_plugin_types();
 169  
 170          foreach ($plugintypes as $plugintype => $fulldir) {
 171              $plugins = core_component::get_plugin_list($plugintype);
 172              foreach ($plugins as $pluginname => $plugindir) {
 173                  $this->assertSame($plugindir, core_component::get_plugin_directory($plugintype, $pluginname));
 174              }
 175          }
 176      }
 177  
 178      public function test_deprecated_get_plugin_directory() {
 179          $plugintypes = core_component::get_plugin_types();
 180  
 181          foreach ($plugintypes as $plugintype => $fulldir) {
 182              $plugins = core_component::get_plugin_list($plugintype);
 183              foreach ($plugins as $pluginname => $plugindir) {
 184                  $this->assertSame(core_component::get_plugin_directory($plugintype, $pluginname), get_plugin_directory($plugintype, $pluginname));
 185              }
 186          }
 187      }
 188  
 189      public function test_get_subsystem_directory() {
 190          $subsystems = core_component::get_core_subsystems();
 191          foreach ($subsystems as $subsystem => $fulldir) {
 192              $this->assertSame($fulldir, core_component::get_subsystem_directory($subsystem));
 193          }
 194      }
 195  
 196      /**
 197       * Test that the get_plugin_list_with_file() function returns the correct list of plugins.
 198       *
 199       * @covers \core_component::is_valid_plugin_name
 200       * @dataProvider is_valid_plugin_name_provider
 201       * @param array $arguments
 202       * @param bool $expected
 203       */
 204      public function test_is_valid_plugin_name(array $arguments, bool $expected): void {
 205          $this->assertEquals($expected, core_component::is_valid_plugin_name(...$arguments));
 206      }
 207  
 208      /**
 209       * Data provider for the is_valid_plugin_name function.
 210       *
 211       * @return array
 212       */
 213      public function is_valid_plugin_name_provider(): array {
 214          return [
 215              [['mod', 'example1'], true],
 216              [['mod', 'feedback360'], true],
 217              [['mod', 'feedback_360'], false],
 218              [['mod', '2feedback'], false],
 219              [['mod', '1example'], false],
 220              [['mod', 'example.xx'], false],
 221              [['mod', '.example'], false],
 222              [['mod', '_example'], false],
 223              [['mod', 'example_'], false],
 224              [['mod', 'example_x1'], false],
 225              [['mod', 'example-x1'], false],
 226              [['mod', 'role'], false],
 227  
 228              [['tool', 'example1'], true],
 229              [['tool', 'example_x1'], true],
 230              [['tool', 'example_x1_xxx'], true],
 231              [['tool', 'feedback360'], true],
 232              [['tool', 'feed_back360'], true],
 233              [['tool', 'role'], true],
 234              [['tool', '1example'], false],
 235              [['tool', 'example.xx'], false],
 236              [['tool', 'example-xx'], false],
 237              [['tool', '.example'], false],
 238              [['tool', '_example'], false],
 239              [['tool', 'example_'], false],
 240              [['tool', 'example__x1'], false],
 241  
 242              // Some invalid cases.
 243              [['mod', null], false],
 244              [['mod', ''], false],
 245              [['tool', null], false],
 246              [['tool', ''], false],
 247          ];
 248      }
 249  
 250      public function test_normalize_componentname() {
 251          // Moodle core.
 252          $this->assertSame('core', core_component::normalize_componentname('core'));
 253          $this->assertSame('core', core_component::normalize_componentname('moodle'));
 254          $this->assertSame('core', core_component::normalize_componentname(''));
 255  
 256          // Moodle core subsystems.
 257          $this->assertSame('core_admin', core_component::normalize_componentname('admin'));
 258          $this->assertSame('core_admin', core_component::normalize_componentname('core_admin'));
 259          $this->assertSame('core_admin', core_component::normalize_componentname('moodle_admin'));
 260  
 261          // Activity modules and their subplugins.
 262          $this->assertSame('mod_workshop', core_component::normalize_componentname('workshop'));
 263          $this->assertSame('mod_workshop', core_component::normalize_componentname('mod_workshop'));
 264          $this->assertSame('workshopform_accumulative', core_component::normalize_componentname('workshopform_accumulative'));
 265          $this->assertSame('mod_quiz', core_component::normalize_componentname('quiz'));
 266          $this->assertSame('quiz_grading', core_component::normalize_componentname('quiz_grading'));
 267          $this->assertSame('mod_data', core_component::normalize_componentname('data'));
 268          $this->assertSame('datafield_checkbox', core_component::normalize_componentname('datafield_checkbox'));
 269  
 270          // Other plugin types.
 271          $this->assertSame('auth_mnet', core_component::normalize_componentname('auth_mnet'));
 272          $this->assertSame('enrol_self', core_component::normalize_componentname('enrol_self'));
 273          $this->assertSame('block_html', core_component::normalize_componentname('block_html'));
 274          $this->assertSame('block_mnet_hosts', core_component::normalize_componentname('block_mnet_hosts'));
 275          $this->assertSame('local_amos', core_component::normalize_componentname('local_amos'));
 276          $this->assertSame('local_admin', core_component::normalize_componentname('local_admin'));
 277  
 278          // Unknown words without underscore are supposed to be activity modules.
 279          $this->assertSame('mod_whoonearthwouldcomewithsuchastupidnameofcomponent',
 280              core_component::normalize_componentname('whoonearthwouldcomewithsuchastupidnameofcomponent'));
 281          // Module names can not contain underscores, this must be a subplugin.
 282          $this->assertSame('whoonearth_wouldcomewithsuchastupidnameofcomponent',
 283              core_component::normalize_componentname('whoonearth_wouldcomewithsuchastupidnameofcomponent'));
 284          $this->assertSame('whoonearth_would_come_withsuchastupidnameofcomponent',
 285              core_component::normalize_componentname('whoonearth_would_come_withsuchastupidnameofcomponent'));
 286      }
 287  
 288      public function test_normalize_component() {
 289          // Moodle core.
 290          $this->assertSame(array('core', null), core_component::normalize_component('core'));
 291          $this->assertSame(array('core', null), core_component::normalize_component('moodle'));
 292          $this->assertSame(array('core', null), core_component::normalize_component(''));
 293  
 294          // Moodle core subsystems.
 295          $this->assertSame(array('core', 'admin'), core_component::normalize_component('admin'));
 296          $this->assertSame(array('core', 'admin'), core_component::normalize_component('core_admin'));
 297          $this->assertSame(array('core', 'admin'), core_component::normalize_component('moodle_admin'));
 298  
 299          // Activity modules and their subplugins.
 300          $this->assertSame(array('mod', 'workshop'), core_component::normalize_component('workshop'));
 301          $this->assertSame(array('mod', 'workshop'), core_component::normalize_component('mod_workshop'));
 302          $this->assertSame(array('workshopform', 'accumulative'), core_component::normalize_component('workshopform_accumulative'));
 303          $this->assertSame(array('mod', 'quiz'), core_component::normalize_component('quiz'));
 304          $this->assertSame(array('quiz', 'grading'), core_component::normalize_component('quiz_grading'));
 305          $this->assertSame(array('mod', 'data'), core_component::normalize_component('data'));
 306          $this->assertSame(array('datafield', 'checkbox'), core_component::normalize_component('datafield_checkbox'));
 307  
 308          // Other plugin types.
 309          $this->assertSame(array('auth', 'mnet'), core_component::normalize_component('auth_mnet'));
 310          $this->assertSame(array('enrol', 'self'), core_component::normalize_component('enrol_self'));
 311          $this->assertSame(array('block', 'html'), core_component::normalize_component('block_html'));
 312          $this->assertSame(array('block', 'mnet_hosts'), core_component::normalize_component('block_mnet_hosts'));
 313          $this->assertSame(array('local', 'amos'), core_component::normalize_component('local_amos'));
 314          $this->assertSame(array('local', 'admin'), core_component::normalize_component('local_admin'));
 315  
 316          // Unknown words without underscore are supposed to be activity modules.
 317          $this->assertSame(array('mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'),
 318              core_component::normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent'));
 319          // Module names can not contain underscores, this must be a subplugin.
 320          $this->assertSame(array('whoonearth', 'wouldcomewithsuchastupidnameofcomponent'),
 321              core_component::normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent'));
 322          $this->assertSame(array('whoonearth', 'would_come_withsuchastupidnameofcomponent'),
 323              core_component::normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent'));
 324      }
 325  
 326      public function test_deprecated_normalize_component() {
 327          // Moodle core.
 328          $this->assertSame(array('core', null), normalize_component('core'));
 329          $this->assertSame(array('core', null), normalize_component(''));
 330          $this->assertSame(array('core', null), normalize_component('moodle'));
 331  
 332          // Moodle core subsystems.
 333          $this->assertSame(array('core', 'admin'), normalize_component('admin'));
 334          $this->assertSame(array('core', 'admin'), normalize_component('core_admin'));
 335          $this->assertSame(array('core', 'admin'), normalize_component('moodle_admin'));
 336  
 337          // Activity modules and their subplugins.
 338          $this->assertSame(array('mod', 'workshop'), normalize_component('workshop'));
 339          $this->assertSame(array('mod', 'workshop'), normalize_component('mod_workshop'));
 340          $this->assertSame(array('workshopform', 'accumulative'), normalize_component('workshopform_accumulative'));
 341          $this->assertSame(array('mod', 'quiz'), normalize_component('quiz'));
 342          $this->assertSame(array('quiz', 'grading'), normalize_component('quiz_grading'));
 343          $this->assertSame(array('mod', 'data'), normalize_component('data'));
 344          $this->assertSame(array('datafield', 'checkbox'), normalize_component('datafield_checkbox'));
 345  
 346          // Other plugin types.
 347          $this->assertSame(array('auth', 'mnet'), normalize_component('auth_mnet'));
 348          $this->assertSame(array('enrol', 'self'), normalize_component('enrol_self'));
 349          $this->assertSame(array('block', 'html'), normalize_component('block_html'));
 350          $this->assertSame(array('block', 'mnet_hosts'), normalize_component('block_mnet_hosts'));
 351          $this->assertSame(array('local', 'amos'), normalize_component('local_amos'));
 352          $this->assertSame(array('local', 'admin'), normalize_component('local_admin'));
 353  
 354          // Unknown words without underscore are supposed to be activity modules.
 355          $this->assertSame(array('mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'),
 356              normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent'));
 357          // Module names can not contain underscores, this must be a subplugin.
 358          $this->assertSame(array('whoonearth', 'wouldcomewithsuchastupidnameofcomponent'),
 359              normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent'));
 360          $this->assertSame(array('whoonearth', 'would_come_withsuchastupidnameofcomponent'),
 361              normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent'));
 362      }
 363  
 364      public function test_get_component_directory() {
 365          $plugintypes = core_component::get_plugin_types();
 366          foreach ($plugintypes as $plugintype => $fulldir) {
 367              $plugins = core_component::get_plugin_list($plugintype);
 368              foreach ($plugins as $pluginname => $plugindir) {
 369                  $this->assertSame($plugindir, core_component::get_component_directory(($plugintype.'_'.$pluginname)));
 370              }
 371          }
 372  
 373          $subsystems = core_component::get_core_subsystems();
 374          foreach ($subsystems as $subsystem => $fulldir) {
 375              $this->assertSame($fulldir, core_component::get_component_directory(('core_'.$subsystem)));
 376          }
 377      }
 378  
 379      public function test_deprecated_get_component_directory() {
 380          $plugintypes = core_component::get_plugin_types();
 381          foreach ($plugintypes as $plugintype => $fulldir) {
 382              $plugins = core_component::get_plugin_list($plugintype);
 383              foreach ($plugins as $pluginname => $plugindir) {
 384                  $this->assertSame($plugindir, get_component_directory(($plugintype.'_'.$pluginname)));
 385              }
 386          }
 387  
 388          $subsystems = core_component::get_core_subsystems();
 389          foreach ($subsystems as $subsystem => $fulldir) {
 390              $this->assertSame($fulldir, get_component_directory(('core_'.$subsystem)));
 391          }
 392      }
 393  
 394      public function test_get_subtype_parent() {
 395          global $CFG;
 396  
 397          $this->assertNull(core_component::get_subtype_parent('mod'));
 398  
 399          // Any plugin with more subtypes is ok here.
 400          $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
 401          $this->assertSame('mod_assign', core_component::get_subtype_parent('assignsubmission'));
 402          $this->assertSame('mod_assign', core_component::get_subtype_parent('assignfeedback'));
 403          $this->assertNull(core_component::get_subtype_parent('assignxxxxx'));
 404      }
 405  
 406      public function test_get_subplugins() {
 407          global $CFG;
 408  
 409          // Any plugin with more subtypes is ok here.
 410          $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
 411  
 412          $subplugins = core_component::get_subplugins('mod_assign');
 413          $this->assertSame(array('assignsubmission', 'assignfeedback'), array_keys($subplugins));
 414  
 415          $subs = core_component::get_plugin_list('assignsubmission');
 416          $feeds = core_component::get_plugin_list('assignfeedback');
 417  
 418          $this->assertSame(array_keys($subs), $subplugins['assignsubmission']);
 419          $this->assertSame(array_keys($feeds), $subplugins['assignfeedback']);
 420  
 421          // Any plugin without subtypes is ok here.
 422          $this->assertFileExists("$CFG->dirroot/mod/choice");
 423          $this->assertFileDoesNotExist("$CFG->dirroot/mod/choice/db/subplugins.json");
 424  
 425          $this->assertNull(core_component::get_subplugins('mod_choice'));
 426  
 427          $this->assertNull(core_component::get_subplugins('xxxx_yyyy'));
 428      }
 429  
 430      public function test_get_plugin_types_with_subplugins() {
 431          global $CFG;
 432  
 433          $types = core_component::get_plugin_types_with_subplugins();
 434  
 435          // Hardcode it here to detect if anybody hacks the code to include more subplugin types.
 436          $expected = array(
 437              'mod' => "$CFG->dirroot/mod",
 438              'editor' => "$CFG->dirroot/lib/editor",
 439              'tool' => "$CFG->dirroot/$CFG->admin/tool",
 440              'local' => "$CFG->dirroot/local",
 441          );
 442  
 443          $this->assertSame($expected, $types);
 444  
 445      }
 446  
 447      public function test_get_plugin_list_with_file() {
 448          $this->resetAfterTest(true);
 449  
 450          // No extra reset here because core_component reset automatically.
 451  
 452          $expected = array();
 453          $reports = core_component::get_plugin_list('report');
 454          foreach ($reports as $name => $fulldir) {
 455              if (file_exists("$fulldir/lib.php")) {
 456                  $expected[] = $name;
 457              }
 458          }
 459  
 460          // Test cold.
 461          $list = core_component::get_plugin_list_with_file('report', 'lib.php', false);
 462          $this->assertEquals($expected, array_keys($list));
 463  
 464          // Test hot.
 465          $list = core_component::get_plugin_list_with_file('report', 'lib.php', false);
 466          $this->assertEquals($expected, array_keys($list));
 467  
 468          // Test with include.
 469          $list = core_component::get_plugin_list_with_file('report', 'lib.php', true);
 470          $this->assertEquals($expected, array_keys($list));
 471  
 472          // Test missing.
 473          $list = core_component::get_plugin_list_with_file('report', 'idontexist.php', true);
 474          $this->assertEquals(array(), array_keys($list));
 475      }
 476  
 477      public function test_get_component_classes_in_namespace() {
 478  
 479          // Unexisting.
 480          $this->assertCount(0, core_component::get_component_classes_in_namespace('core_unexistingcomponent', 'something'));
 481          $this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'something'));
 482  
 483          // Matches the last namespace level name not partials.
 484          $this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'tas'));
 485          $this->assertCount(0, core_component::get_component_classes_in_namespace('core_user', 'course'));
 486          $this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', 'output\\emaildigest'));
 487          $this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\emaildigest'));
 488          $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', 'output\\email'));
 489          $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\email'));
 490          $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', 'output\\email\\'));
 491          $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\email\\'));
 492  
 493          // Prefix with backslash if it doesn\'t come prefixed.
 494          $this->assertCount(1, core_component::get_component_classes_in_namespace('auth_cas', 'task'));
 495          $this->assertCount(1, core_component::get_component_classes_in_namespace('auth_cas', '\\task'));
 496  
 497          // Core as a component works, the function can normalise the component name.
 498          $this->assertCount(7, core_component::get_component_classes_in_namespace('core', 'update'));
 499          $this->assertCount(7, core_component::get_component_classes_in_namespace('', 'update'));
 500          $this->assertCount(7, core_component::get_component_classes_in_namespace('moodle', 'update'));
 501  
 502          // Multiple levels.
 503          $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile\\'));
 504          $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile\\'));
 505          $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile'));
 506          $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile'));
 507  
 508          // Without namespace it returns classes/ classes.
 509          $this->assertCount(5, core_component::get_component_classes_in_namespace('tool_mobile', ''));
 510          $this->assertCount(2, core_component::get_component_classes_in_namespace('tool_filetypes'));
 511  
 512          // When no component is specified, classes are returned for the namespace in all components.
 513          // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed).
 514          $this->assertGreaterThan(
 515              count(\core_component::get_component_classes_in_namespace('core', 'output')),
 516              count(\core_component::get_component_classes_in_namespace(null, 'output')));
 517  
 518          // Without either a component or namespace it returns an empty array.
 519          $this->assertEmpty(\core_component::get_component_classes_in_namespace());
 520          $this->assertEmpty(\core_component::get_component_classes_in_namespace(null));
 521          $this->assertEmpty(\core_component::get_component_classes_in_namespace(null, ''));
 522      }
 523  
 524      /**
 525       * Data provider for classloader test
 526       */
 527      public function classloader_provider() {
 528          global $CFG;
 529  
 530          // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
 531          // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
 532          // If problems arise we can remove this test, but will need to add a warning.
 533          // Normalise to forward slash for testing purposes.
 534          $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
 535  
 536          $psr0 = [
 537            'psr0'      => 'lib/tests/fixtures/component/psr0',
 538            'overlap'   => 'lib/tests/fixtures/component/overlap'
 539          ];
 540          $psr4 = [
 541            'psr4'      => 'lib/tests/fixtures/component/psr4',
 542            'overlap'   => 'lib/tests/fixtures/component/overlap'
 543          ];
 544          return [
 545            'PSR-0 Classloading - Root' => [
 546                'psr0' => $psr0,
 547                'psr4' => $psr4,
 548                'classname' => 'psr0_main',
 549                'includedfiles' => "{$directory}psr0/main.php",
 550            ],
 551            'PSR-0 Classloading - Sub namespace - underscores' => [
 552                'psr0' => $psr0,
 553                'psr4' => $psr4,
 554                'classname' => 'psr0_subnamespace_example',
 555                'includedfiles' => "{$directory}psr0/subnamespace/example.php",
 556            ],
 557            'PSR-0 Classloading - Sub namespace - slashes' => [
 558                'psr0' => $psr0,
 559                'psr4' => $psr4,
 560                'classname' => 'psr0\\subnamespace\\slashes',
 561                'includedfiles' => "{$directory}psr0/subnamespace/slashes.php",
 562            ],
 563            'PSR-4 Classloading - Root' => [
 564                'psr0' => $psr0,
 565                'psr4' => $psr4,
 566                'classname' => 'psr4\\main',
 567                'includedfiles' => "{$directory}psr4/main.php",
 568            ],
 569            'PSR-4 Classloading - Sub namespace' => [
 570                'psr0' => $psr0,
 571                'psr4' => $psr4,
 572                'classname' => 'psr4\\subnamespace\\example',
 573                'includedfiles' => "{$directory}psr4/subnamespace/example.php",
 574            ],
 575            'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
 576                'psr0' => $psr0,
 577                'psr4' => $psr4,
 578                'classname' => 'psr4\\subnamespace\\underscore_example',
 579                'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php",
 580            ],
 581            'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
 582                'psr0' => $psr0,
 583                'psr4' => $psr4,
 584                'classname' => 'overlap\\subnamespace\\example',
 585                'includedfiles' => "{$directory}overlap/subnamespace/example.php",
 586            ],
 587            'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
 588                'psr0' => $psr0,
 589                'psr4' => $psr4,
 590                'classname' => 'overlap_subnamespace_example2',
 591                'includedfiles' => "{$directory}overlap/subnamespace/example2.php",
 592            ],
 593          ];
 594      }
 595  
 596      /**
 597       * Test the classloader.
 598       *
 599       * @dataProvider classloader_provider
 600       * @param array $psr0 The PSR-0 namespaces to be used in the test.
 601       * @param array $psr4 The PSR-4 namespaces to be used in the test.
 602       * @param string $classname The name of the class to attempt to load.
 603       * @param string $includedfiles The file expected to be loaded.
 604       * @runInSeparateProcess
 605       */
 606      public function test_classloader($psr0, $psr4, $classname, $includedfiles) {
 607          $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
 608          $psr0namespaces->setAccessible(true);
 609          $psr0namespaces->setValue(null, $psr0);
 610  
 611          $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
 612          $psr4namespaces->setAccessible(true);
 613          $psr4namespaces->setValue(null, $psr4);
 614  
 615          core_component::classloader($classname);
 616          if (DIRECTORY_SEPARATOR != '/') {
 617              // Denormalise the expected path so that we can quickly compare with get_included_files.
 618              $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles);
 619          }
 620          $this->assertContains($includedfiles, get_included_files());
 621          $this->assertTrue(class_exists($classname, false));
 622      }
 623  
 624      /**
 625       * Data provider for psr_classloader test
 626       */
 627      public function psr_classloader_provider() {
 628          global $CFG;
 629  
 630          // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
 631          // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
 632          // If problems arise we can remove this test, but will need to add a warning.
 633          // Normalise to forward slash for testing purposes.
 634          $dirroot = str_replace('\\', '/', $CFG->dirroot);
 635          $directory = "{$dirroot}/lib/tests/fixtures/component/";
 636  
 637          $psr0 = [
 638            'psr0'      => 'lib/tests/fixtures/component/psr0',
 639            'overlap'   => 'lib/tests/fixtures/component/overlap'
 640          ];
 641          $psr4 = [
 642            'psr4'      => 'lib/tests/fixtures/component/psr4',
 643            'overlap'   => 'lib/tests/fixtures/component/overlap'
 644          ];
 645          return [
 646            'PSR-0 Classloading - Root' => [
 647                'psr0' => $psr0,
 648                'psr4' => $psr4,
 649                'classname' => 'psr0_main',
 650                'file' => "{$directory}psr0/main.php",
 651            ],
 652            'PSR-0 Classloading - Sub namespace - underscores' => [
 653                'psr0' => $psr0,
 654                'psr4' => $psr4,
 655                'classname' => 'psr0_subnamespace_example',
 656                'file' => "{$directory}psr0/subnamespace/example.php",
 657            ],
 658            'PSR-0 Classloading - Sub namespace - slashes' => [
 659                'psr0' => $psr0,
 660                'psr4' => $psr4,
 661                'classname' => 'psr0\\subnamespace\\slashes',
 662                'file' => "{$directory}psr0/subnamespace/slashes.php",
 663            ],
 664            'PSR-0 Classloading - non-existant file' => [
 665                'psr0' => $psr0,
 666                'psr4' => $psr4,
 667                'classname' => 'psr0_subnamespace_nonexistant_file',
 668                'file' => false,
 669            ],
 670            'PSR-4 Classloading - Root' => [
 671                'psr0' => $psr0,
 672                'psr4' => $psr4,
 673                'classname' => 'psr4\\main',
 674                'file' => "{$directory}psr4/main.php",
 675            ],
 676            'PSR-4 Classloading - Sub namespace' => [
 677                'psr0' => $psr0,
 678                'psr4' => $psr4,
 679                'classname' => 'psr4\\subnamespace\\example',
 680                'file' => "{$directory}psr4/subnamespace/example.php",
 681            ],
 682            'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
 683                'psr0' => $psr0,
 684                'psr4' => $psr4,
 685                'classname' => 'psr4\\subnamespace\\underscore_example',
 686                'file' => "{$directory}psr4/subnamespace/underscore_example.php",
 687            ],
 688            'PSR-4 Classloading - non-existant file' => [
 689                'psr0' => $psr0,
 690                'psr4' => $psr4,
 691                'classname' => 'psr4\\subnamespace\\nonexistant',
 692                'file' => false,
 693            ],
 694            'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
 695                'psr0' => $psr0,
 696                'psr4' => $psr4,
 697                'classname' => 'overlap\\subnamespace\\example',
 698                'file' => "{$directory}overlap/subnamespace/example.php",
 699            ],
 700            'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
 701                'psr0' => $psr0,
 702                'psr4' => $psr4,
 703                'classname' => 'overlap_subnamespace_example2',
 704                'file' => "{$directory}overlap/subnamespace/example2.php",
 705            ],
 706              'PSR-4 namespaces can come from multiple sources - first source' => [
 707                  'psr0' => $psr0,
 708                  'psr4' => [
 709                      'Psr\\Http\\Message' => [
 710                          'lib/psr/http-message/src',
 711                          'lib/psr/http-factory/src',
 712                      ],
 713                  ],
 714                  'classname' => 'Psr\Http\Message\ServerRequestInterface',
 715                  'includedfiles' => "{$dirroot}/lib/psr/http-message/src/ServerRequestInterface.php",
 716              ],
 717              'PSR-4 namespaces can come from multiple sources - second source' => [
 718                  'psr0' => [],
 719                  'psr4' => [
 720                      'Psr\\Http\\Message' => [
 721                          'lib/psr/http-message/src',
 722                          'lib/psr/http-factory/src',
 723                      ],
 724                  ],
 725                  'classname' => 'Psr\Http\Message\ServerRequestFactoryInterface',
 726                  'includedfiles' => "{$dirroot}/lib/psr/http-factory/src/ServerRequestFactoryInterface.php",
 727              ],
 728          ];
 729      }
 730  
 731      /**
 732       * Test the PSR classloader.
 733       *
 734       * @dataProvider psr_classloader_provider
 735       * @param array $psr0 The PSR-0 namespaces to be used in the test.
 736       * @param array $psr4 The PSR-4 namespaces to be used in the test.
 737       * @param string $classname The name of the class to attempt to load.
 738       * @param string|bool $file The expected file corresponding to the class or false for nonexistant.
 739       * @runInSeparateProcess
 740       */
 741      public function test_psr_classloader($psr0, $psr4, $classname, $file) {
 742          $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
 743          $psr0namespaces->setAccessible(true);
 744          $psr0namespaces->setValue(null, $psr0);
 745  
 746          $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
 747          $psr4namespaces->setAccessible(true);
 748          $oldpsr4namespaces = $psr4namespaces->getValue(null);
 749          $psr4namespaces->setValue(null, $psr4);
 750  
 751          $component = new ReflectionClass('core_component');
 752          $psrclassloader = $component->getMethod('psr_classloader');
 753          $psrclassloader->setAccessible(true);
 754  
 755          $returnvalue = $psrclassloader->invokeArgs(null, array($classname));
 756          // Normalise to forward slashes for testing comparison.
 757          if ($returnvalue) {
 758              $returnvalue = str_replace('\\', '/', $returnvalue);
 759          }
 760          $this->assertEquals($file, $returnvalue);
 761      }
 762  
 763      /**
 764       * Data provider for get_class_file test
 765       */
 766      public function get_class_file_provider() {
 767          global $CFG;
 768  
 769          return [
 770            'Getting a file with underscores' => [
 771                'classname' => 'Test_With_Underscores',
 772                'prefix' => "Test",
 773                'path' => 'test/src',
 774                'separators' => ['_'],
 775                'result' => $CFG->dirroot . "/test/src/With/Underscores.php",
 776            ],
 777            'Getting a file with slashes' => [
 778                'classname' => 'Test\\With\\Slashes',
 779                'prefix' => "Test",
 780                'path' => 'test/src',
 781                'separators' => ['\\'],
 782                'result' => $CFG->dirroot . "/test/src/With/Slashes.php",
 783            ],
 784            'Getting a file with multiple namespaces' => [
 785                'classname' => 'Test\\With\\Multiple\\Namespaces',
 786                'prefix' => "Test\\With",
 787                'path' => 'test/src',
 788                'separators' => ['\\'],
 789                'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php",
 790            ],
 791            'Getting a file with multiple namespaces (non-existent)' => [
 792                'classname' => 'Nonexistant\\Namespace\\Test',
 793                'prefix' => "Test",
 794                'path' => 'test/src',
 795                'separators' => ['\\'],
 796                'result' => false,
 797            ],
 798          ];
 799      }
 800  
 801      /**
 802       * Test the PSR classloader.
 803       *
 804       * @dataProvider get_class_file_provider
 805       * @param string $classname the name of the class.
 806       * @param string $prefix The namespace prefix used to identify the base directory of the source files.
 807       * @param string $path The relative path to the base directory of the source files.
 808       * @param string[] $separators The characters that should be used for separating.
 809       * @param string|bool $result The expected result to be returned from get_class_file.
 810       */
 811      public function test_get_class_file($classname, $prefix, $path, $separators, $result) {
 812          $component = new ReflectionClass('core_component');
 813          $psrclassloader = $component->getMethod('get_class_file');
 814          $psrclassloader->setAccessible(true);
 815  
 816          $file = $psrclassloader->invokeArgs(null, array($classname, $prefix, $path, $separators));
 817          $this->assertEquals($result, $file);
 818      }
 819  
 820      /**
 821       * Confirm the get_component_list method contains an entry for every component.
 822       */
 823      public function test_get_component_list_contains_all_components() {
 824          global $CFG;
 825          $componentslist = \core_component::get_component_list();
 826  
 827          // We should have an entry for each plugin type, and one additional for 'core'.
 828          $plugintypes = \core_component::get_plugin_types();
 829          $numelementsexpected = count($plugintypes) + 1;
 830          $this->assertEquals($numelementsexpected, count($componentslist));
 831  
 832          // And an entry for each of the plugin types.
 833          foreach (array_keys($plugintypes) as $plugintype) {
 834              $this->assertArrayHasKey($plugintype, $componentslist);
 835          }
 836  
 837          // And finally, one for 'core'.
 838          $this->assertArrayHasKey('core', $componentslist);
 839  
 840          // Check a few of the known plugin types to confirm their presence at their respective type index.
 841          $this->assertEquals($componentslist['core']['core_comment'], $CFG->dirroot . '/comment');
 842          $this->assertEquals($componentslist['mod']['mod_forum'], $CFG->dirroot . '/mod/forum');
 843          $this->assertEquals($componentslist['tool']['tool_usertours'], $CFG->dirroot . '/' . $CFG->admin . '/tool/usertours');
 844      }
 845  
 846      /**
 847       * Test the get_component_names() method.
 848       */
 849      public function test_get_component_names() {
 850          global $CFG;
 851          $componentnames = \core_component::get_component_names();
 852  
 853          // We should have an entry for each plugin type.
 854          $plugintypes = \core_component::get_plugin_types();
 855          $numplugintypes = 0;
 856          foreach ($plugintypes as $type => $typedir) {
 857              foreach (\core_component::get_plugin_list($type) as $plugin) {
 858                  $numplugintypes++;
 859              }
 860          }
 861          // And an entry for each core subsystem.
 862          $numcomponents = $numplugintypes + count(\core_component::get_core_subsystems());
 863  
 864          $this->assertEquals($numcomponents, count($componentnames));
 865  
 866          // Check a few of the known plugin types to confirm their presence at their respective type index.
 867          $this->assertContains('core_comment', $componentnames);
 868          $this->assertContains('mod_forum', $componentnames);
 869          $this->assertContains('tool_usertours', $componentnames);
 870          $this->assertContains('core_favourites', $componentnames);
 871      }
 872  
 873      /**
 874       * Basic tests for APIs related functions in the core_component class.
 875       */
 876      public function test_apis_methods() {
 877          $apis = core_component::get_core_apis();
 878          $this->assertIsArray($apis);
 879  
 880          $apinames = core_component::get_core_api_names();
 881          $this->assertIsArray($apis);
 882  
 883          // Both should return the very same APIs.
 884          $this->assertEquals($apinames, array_keys($apis));
 885  
 886          $this->assertFalse(core_component::is_core_api('lalala'));
 887          $this->assertTrue(core_component::is_core_api('privacy'));
 888      }
 889  
 890      /**
 891       * Test that the apis.json structure matches expectations
 892       *
 893       * While we include an apis.schema.json file in core, there isn't any PHP built-in allowing us
 894       * to validate it (3rd part libraries needed). Plus the schema doesn't allow to validate things
 895       * like uniqueness or sorting. We are going to do all that here.
 896       */
 897      public function test_apis_json_validation() {
 898          $apis = $sortedapis = core_component::get_core_apis();
 899          ksort($sortedapis); // We'll need this later.
 900  
 901          $subsystems = core_component::get_core_subsystems(); // To verify all apis are pointing to valid subsystems.
 902          $subsystems['core'] = 'anything'; // Let's add 'core' because it's a valid component for apis.
 903  
 904          // General structure validations.
 905          $this->assertIsArray($apis);
 906          $this->assertGreaterThan(25, count($apis));
 907          $this->assertArrayHasKey('privacy', $apis); // Verify a few.
 908          $this->assertArrayHasKey('external', $apis);
 909          $this->assertArrayHasKey('search', $apis);
 910          $this->assertEquals(array_keys($sortedapis), array_keys($apis)); // Verify json is sorted alphabetically.
 911  
 912          // Iterate over all apis and perform more validations.
 913          foreach ($apis as $apiname => $attributes) {
 914              // Message, to be used later and easier finding the problem.
 915              $message = "Validation problem found with API: {$apiname}";
 916  
 917              $this->assertIsObject($attributes, $message);
 918              $this->assertMatchesRegularExpression('/^[a-z][a-z0-9]+$/', $apiname, $message);
 919              $this->assertEquals(['component', 'allowedlevel2', 'allowedspread'], array_keys((array)$attributes), $message);
 920  
 921              // Verify attributes.
 922              if ($apiname !== 'core') { // Exception for core api, it doesn't have component.
 923                  // Check that component attribute looks correct.
 924                  $this->assertMatchesRegularExpression('/^(core|[a-z][a-z0-9_]+)$/', $attributes->component, $message);
 925                  // Ensure that the api component (without the core_ prefix) is a correct subsystem.
 926                  $this->assertArrayHasKey(str_replace('core_', '', $attributes->component), $subsystems, $message);
 927              } else {
 928                  $this->assertNull($attributes->component, $message);
 929              }
 930  
 931  
 932              // Now check for the rest of attributes.
 933              $this->assertIsBool($attributes->allowedlevel2, $message);
 934              $this->assertIsBool($attributes->allowedspread, $message);
 935  
 936              // Cannot spread if level2 is not allowed.
 937              $this->assertLessThanOrEqual($attributes->allowedlevel2, $attributes->allowedspread, $message);
 938          }
 939      }
 940  
 941      /**
 942       * Test for monologo icons check in plugins.
 943       *
 944       * @covers core_component::has_monologo_icon
 945       * @return void
 946       */
 947      public function test_has_monologo_icon(): void {
 948          // The Forum activity plugin has monologo icons.
 949          $this->assertTrue(core_component::has_monologo_icon('mod', 'forum'));
 950          // The core H5P subsystem doesn't have monologo icons.
 951          $this->assertFalse(core_component::has_monologo_icon('core', 'h5p'));
 952          // The function will return false for a non-existent component.
 953          $this->assertFalse(core_component::has_monologo_icon('randomcomponent', 'h5p'));
 954      }
 955  
 956      /*
 957       * Tests the getter for the db directory summary hash.
 958       *
 959       * @covers \core_component::get_all_directory_hashes
 960       */
 961      public function test_get_db_directories_hash() {
 962          $initial = \core_component::get_all_component_hash();
 963  
 964          $dir = make_request_directory();
 965          $hashes = \core_component::get_all_directory_hashes([$dir]);
 966          $emptydirhash = \core_component::get_all_component_hash([$hashes]);
 967  
 968          // Confirm that a single empty directory is a different hash to the core hash.
 969          $this->assertNotEquals($initial, $emptydirhash);
 970  
 971          // Now lets add something to the dir, and check the hash is different.
 972          $file = fopen($dir . '/test.php', 'w');
 973          fwrite($file, 'sometestdata');
 974          fclose($file);
 975  
 976          $hashes = \core_component::get_all_directory_hashes([$dir]);
 977          $onefiledirhash = \core_component::get_all_component_hash([$hashes]);
 978          $this->assertNotEquals($emptydirhash, $onefiledirhash);
 979  
 980          // Now add a subdirectory inside the request dir. This should not affect the hash.
 981          mkdir($dir . '/subdir');
 982          $hashes = \core_component::get_all_directory_hashes([$dir]);
 983          $finalhash = \core_component::get_all_component_hash([$hashes]);
 984          $this->assertEquals($onefiledirhash, $finalhash);
 985      }
 986  }