Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->libdir . '/ldaplib.php');
  23  
  24  /**
  25   * ldap tests.
  26   *
  27   * @package    core
  28   * @category   test
  29   * @copyright  Damyon Wiese, Iñaki Arenaza 2014
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  31   */
  32  class ldaplib_test extends \advanced_testcase {
  33  
  34      public function test_ldap_addslashes() {
  35          // See http://tools.ietf.org/html/rfc4514#section-5.2 if you want
  36          // to add additional tests.
  37  
  38          $tests = array(
  39              array (
  40                  'test' => 'Simplest',
  41                  'expected' => 'Simplest',
  42              ),
  43              array (
  44                  'test' => 'Simple case',
  45                  'expected' => 'Simple\\20case',
  46              ),
  47              array (
  48                  'test' => 'Medium ‒ case',
  49                  'expected' => 'Medium\\20‒\\20case',
  50              ),
  51              array (
  52                  'test' => '#Harder+case#',
  53                  'expected' => '\\23Harder\\2bcase\\23',
  54              ),
  55              array (
  56                  'test' => ' Harder (and); harder case ',
  57                  'expected' => '\\20Harder\\20(and)\\3b\\20harder\\20case\\20',
  58              ),
  59              array (
  60                  'test' => 'Really \\0 (hard) case!\\',
  61                  'expected' => 'Really\\20\\5c0\\20(hard)\\20case!\\5c',
  62              ),
  63              array (
  64                  'test' => 'James "Jim" = Smith, III',
  65                  'expected' => 'James\\20\\22Jim\22\\20\\3d\\20Smith\\2c\\20III',
  66              ),
  67              array (
  68                  'test' => '  <jsmith@example.com> ',
  69                  'expected' => '\\20\\20\\3cjsmith@example.com\\3e\\20',
  70              ),
  71          );
  72  
  73  
  74          foreach ($tests as $test) {
  75              $this->assertSame($test['expected'], ldap_addslashes($test['test']));
  76          }
  77      }
  78  
  79      public function test_ldap_stripslashes() {
  80          // See http://tools.ietf.org/html/rfc4514#section-5.2 if you want
  81          // to add additional tests.
  82  
  83          // IMPORTANT NOTICE: While ldap_addslashes() only produces one
  84          // of the two defined ways of escaping/quoting (the ESC HEX
  85          // HEX way defined in the grammar in Section 3 of RFC-4514)
  86          // ldap_stripslashes() has to deal with both of them. So in
  87          // addition to testing the same strings we test in
  88          // test_ldap_stripslashes(), we need to also test strings
  89          // using the second method.
  90  
  91          $tests = array(
  92              array (
  93                  'test' => 'Simplest',
  94                  'expected' => 'Simplest',
  95              ),
  96              array (
  97                  'test' => 'Simple\\20case',
  98                  'expected' => 'Simple case',
  99              ),
 100              array (
 101                  'test' => 'Simple\\ case',
 102                  'expected' => 'Simple case',
 103              ),
 104              array (
 105                  'test' => 'Simple\\ \\63\\61\\73\\65',
 106                  'expected' => 'Simple case',
 107              ),
 108              array (
 109                  'test' => 'Medium\\ ‒\\ case',
 110                  'expected' => 'Medium ‒ case',
 111              ),
 112              array (
 113                  'test' => 'Medium\\20‒\\20case',
 114                  'expected' => 'Medium ‒ case',
 115              ),
 116              array (
 117                  'test' => 'Medium\\20\\E2\\80\\92\\20case',
 118                  'expected' => 'Medium ‒ case',
 119              ),
 120              array (
 121                  'test' => '\\23Harder\\2bcase\\23',
 122                  'expected' => '#Harder+case#',
 123              ),
 124              array (
 125                  'test' => '\\#Harder\\+case\\#',
 126                  'expected' => '#Harder+case#',
 127              ),
 128              array (
 129                  'test' => '\\20Harder\\20(and)\\3b\\20harder\\20case\\20',
 130                  'expected' => ' Harder (and); harder case ',
 131              ),
 132              array (
 133                  'test' => '\\ Harder\\ (and)\\;\\ harder\\ case\\ ',
 134                  'expected' => ' Harder (and); harder case ',
 135              ),
 136              array (
 137                  'test' => 'Really\\20\\5c0\\20(hard)\\20case!\\5c',
 138                  'expected' => 'Really \\0 (hard) case!\\',
 139              ),
 140              array (
 141                  'test' => 'Really\\ \\\\0\\ (hard)\\ case!\\\\',
 142                  'expected' => 'Really \\0 (hard) case!\\',
 143              ),
 144              array (
 145                  'test' => 'James\\20\\22Jim\\22\\20\\3d\\20Smith\\2c\\20III',
 146                  'expected' => 'James "Jim" = Smith, III',
 147              ),
 148              array (
 149                  'test' => 'James\\ \\"Jim\\" \\= Smith\\, III',
 150                  'expected' => 'James "Jim" = Smith, III',
 151              ),
 152              array (
 153                  'test' => '\\20\\20\\3cjsmith@example.com\\3e\\20',
 154                  'expected' => '  <jsmith@example.com> ',
 155              ),
 156              array (
 157                  'test' => '\\ \\<jsmith@example.com\\>\\ ',
 158                  'expected' => ' <jsmith@example.com> ',
 159              ),
 160              array (
 161                  'test' => 'Lu\\C4\\8Di\\C4\\87',
 162                  'expected' => 'Lučić',
 163              ),
 164          );
 165  
 166          foreach ($tests as $test) {
 167              $this->assertSame($test['expected'], ldap_stripslashes($test['test']));
 168          }
 169      }
 170  
 171      /**
 172       * Tests for ldap_normalise_objectclass.
 173       *
 174       * @dataProvider ldap_normalise_objectclass_provider
 175       * @param array $args Arguments passed to ldap_normalise_objectclass
 176       * @param string $expected The expected objectclass filter
 177       */
 178      public function test_ldap_normalise_objectclass($args, $expected) {
 179          $this->assertEquals($expected, call_user_func_array('ldap_normalise_objectclass', $args));
 180      }
 181  
 182      /**
 183       * Data provider for the test_ldap_normalise_objectclass testcase.
 184       *
 185       * @return array of testcases.
 186       */
 187      public function ldap_normalise_objectclass_provider() {
 188          return array(
 189              'Empty value' => array(
 190                  array(null),
 191                  '(objectClass=*)',
 192              ),
 193              'Empty value with different default' => array(
 194                  array(null, 'lion'),
 195                  '(objectClass=lion)',
 196              ),
 197              'Supplied unwrapped objectClass' => array(
 198                  array('objectClass=tiger'),
 199                  '(objectClass=tiger)',
 200              ),
 201              'Supplied string value' => array(
 202                  array('leopard'),
 203                  '(objectClass=leopard)',
 204              ),
 205              'Supplied complex' => array(
 206                  array('(&(objectClass=cheetah)(enabledMoodleUser=1))'),
 207                  '(&(objectClass=cheetah)(enabledMoodleUser=1))',
 208              ),
 209          );
 210      }
 211  
 212      /**
 213       * Tests for ldap_get_entries_moodle.
 214       *
 215       * NOTE: in order to execute this test you need to set up OpenLDAP server with core,
 216       *       cosine, nis and internet schemas and add configuration constants to
 217       *       config.php or phpunit.xml configuration file.  The bind users *needs*
 218       *       permissions to create objects in the LDAP server, under the bind domain.
 219       *
 220       * define('TEST_LDAPLIB_HOST_URL', 'ldap://127.0.0.1');
 221       * define('TEST_LDAPLIB_BIND_DN', 'cn=someuser,dc=example,dc=local');
 222       * define('TEST_LDAPLIB_BIND_PW', 'somepassword');
 223       * define('TEST_LDAPLIB_DOMAIN',  'dc=example,dc=local');
 224       *
 225       */
 226      public function test_ldap_get_entries_moodle() {
 227          $this->resetAfterTest();
 228  
 229          if (!defined('TEST_LDAPLIB_HOST_URL') or !defined('TEST_LDAPLIB_BIND_DN') or
 230                  !defined('TEST_LDAPLIB_BIND_PW') or !defined('TEST_LDAPLIB_DOMAIN')) {
 231              $this->markTestSkipped('External LDAP test server not configured.');
 232          }
 233  
 234          // Make sure we can connect the server.
 235          $debuginfo = '';
 236          if (!$connection = ldap_connect_moodle(TEST_LDAPLIB_HOST_URL, 3, 'rfc2307', TEST_LDAPLIB_BIND_DN,
 237                                                 TEST_LDAPLIB_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
 238              $this->markTestSkipped('Cannot connect to LDAP test server: '.$debuginfo);
 239          }
 240  
 241          // Create new empty test container.
 242          if (!($containerdn = $this->create_test_container($connection, 'moodletest'))) {
 243              $this->markTestSkipped('Can not create test LDAP container.');
 244          }
 245  
 246          // Add all the test objects.
 247          $testobjects = $this->get_ldap_get_entries_moodle_test_objects();
 248          if (!$this->add_test_objects($connection, $containerdn, $testobjects)) {
 249              $this->markTestSkipped('Can not create LDAP test objects.');
 250          }
 251  
 252          // Now query about them and compare results.
 253          foreach ($testobjects as $object) {
 254              $dn = $this->get_object_dn($object, $containerdn);
 255              $filter = $object['query']['filter'];
 256              $attributes = $object['query']['attributes'];
 257  
 258              $sr = ldap_read($connection, $dn, $filter, $attributes);
 259              if (!$sr) {
 260                  $this->markTestSkipped('Cannot retrieve test objects from LDAP test server.');
 261              }
 262  
 263              $entries = ldap_get_entries_moodle($connection, $sr);
 264              $actual = array_keys($entries[0]);
 265              $expected = $object['expected'];
 266  
 267              // We need to sort both arrays to be able to compare them, as the LDAP server
 268              // might return attributes in any order.
 269              sort($expected);
 270              sort($actual);
 271              $this->assertEquals($expected, $actual);
 272          }
 273  
 274          // Clean up test objects and container.
 275          $this->remove_test_objects($connection, $containerdn, $testobjects);
 276          $this->remove_test_container($connection, $containerdn);
 277      }
 278  
 279      /**
 280       * Provide the array of test objects for the ldap_get_entries_moodle test case.
 281       *
 282       * @return array of test objects
 283       */
 284      protected function get_ldap_get_entries_moodle_test_objects() {
 285          $testobjects = array(
 286              // Test object 1.
 287              array(
 288                  // Add/remove this object to LDAP directory? There are existing standard LDAP
 289                  // objects that we might want to test, but that we shouldn't add/remove ourselves.
 290                  'addremove' => true,
 291                  // Relative (to test container) or absolute distinguished name (DN).
 292                  'relativedn' => true,
 293                  // Distinguished name for this object (interpretation depends on 'relativedn').
 294                  'dn' => 'cn=test1',
 295                  // Values to add to LDAP directory.
 296                  'values' => array(
 297                      'objectClass' => array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount'),
 298                      'cn' => 'test1',  // We don't care about the actual values, as long as they are unique.
 299                      'sn' => 'test1',
 300                      'givenName' => 'test1',
 301                      'uid' => 'test1',
 302                      'uidNumber' => '20001',  // Start from 20000, then add test number.
 303                      'gidNumber' => '20001',  // Start from 20000, then add test number.
 304                      'homeDirectory' => '/',
 305                      'userPassword' => '*',
 306                  ),
 307                  // Attributes to query the object for.
 308                  'query' => array(
 309                      'filter' => '(objectClass=posixAccount)',
 310                      'attributes' => array(
 311                          'cn',
 312                          'sn',
 313                          'givenName',
 314                          'uid',
 315                          'uidNumber',
 316                          'gidNumber',
 317                          'homeDirectory',
 318                          'userPassword'
 319                      ),
 320                  ),
 321                  // Expected values for the queried attributes' names.
 322                  'expected' => array(
 323                      'cn',
 324                      'sn',
 325                      'givenname',
 326                      'uid',
 327                      'uidnumber',
 328                      'gidnumber',
 329                      'homedirectory',
 330                      'userpassword'
 331                  ),
 332              ),
 333              // Test object 2.
 334              array(
 335                  'addremove' => true,
 336                  'relativedn' => true,
 337                  'dn' => 'cn=group2',
 338                  'values' => array(
 339                      'objectClass' => array('top', 'posixGroup'),
 340                      'cn' => 'group2',  // We don't care about the actual values, as long as they are unique.
 341                      'gidNumber' => '20002',  // Start from 20000, then add test number.
 342                      'memberUid' => '20002',  // Start from 20000, then add test number.
 343                  ),
 344                  'query' => array(
 345                      'filter' => '(objectClass=posixGroup)',
 346                      'attributes' => array(
 347                          'cn',
 348                          'gidNumber',
 349                          'memberUid'
 350                      ),
 351                  ),
 352                  'expected' => array(
 353                      'cn',
 354                      'gidnumber',
 355                      'memberuid'
 356                  ),
 357              ),
 358              // Test object 3.
 359              array(
 360                  'addremove' => false,
 361                  'relativedn' => false,
 362                  'dn' => '',  // To query the RootDSE, we must specify the empty string as the absolute DN.
 363                  'values' => array(
 364                  ),
 365                  'query' => array(
 366                      'filter' => '(objectClass=*)',
 367                      'attributes' => array(
 368                          'supportedControl',
 369                          'namingContexts'
 370                      ),
 371                  ),
 372                  'expected' => array(
 373                      'supportedcontrol',
 374                      'namingcontexts'
 375                  ),
 376              ),
 377          );
 378  
 379          return $testobjects;
 380      }
 381  
 382      /**
 383       * Create a new container in the LDAP domain, to hold the test objects. The
 384       * container is created as a domain component (dc) + organizational unit (ou) object.
 385       *
 386       * @param object $connection Valid LDAP connection
 387       * @param string $container Name of the test container to create.
 388       *
 389       * @return string or false Distinguished name for the created container, or false on error.
 390       */
 391      protected function create_test_container($connection, $container) {
 392          $object = array();
 393          $object['objectClass'] = array('dcObject', 'organizationalUnit');
 394          $object['dc'] = $container;
 395          $object['ou'] = $container;
 396          $containerdn = 'dc='.$container.','.TEST_LDAPLIB_DOMAIN;
 397          if (!ldap_add($connection, $containerdn, $object)) {
 398              return false;
 399          }
 400          return $containerdn;
 401      }
 402  
 403      /**
 404       * Remove the container in the LDAP domain root that holds the test objects. The container
 405       * *must* be empty before trying to remove it. Otherwise this function fails.
 406       *
 407       * @param object $connection Valid LDAP connection
 408       * @param string $containerdn The distinguished of the container to remove.
 409       */
 410      protected function remove_test_container($connection, $containerdn) {
 411          ldap_delete($connection, $containerdn);
 412      }
 413  
 414      /**
 415       * Add the test objects to the test container.
 416       *
 417       * @param resource $connection Valid LDAP connection
 418       * @param string $containerdn The distinguished name of the container for the created objects.
 419       * @param array $testobjects Array of the tests objects to create. The structure of
 420       *              the array elements *must* follow the structure of the value returned
 421       *              by ldap_get_entries_moodle_test_objects() member function.
 422       *
 423       * @return boolean True on success, false otherwise.
 424       */
 425      protected function add_test_objects($connection, $containerdn, $testobjects) {
 426          foreach ($testobjects as $object) {
 427              if ($object['addremove'] !== true) {
 428                  continue;
 429              }
 430              $dn = $this->get_object_dn($object, $containerdn);
 431              $entry = $object['values'];
 432              if (!ldap_add($connection, $dn, $entry)) {
 433                  return false;
 434              }
 435          }
 436          return true;
 437      }
 438  
 439      /**
 440       * Remove the test objects from the test container.
 441       *
 442       * @param resource $connection Valid LDAP connection
 443       * @param string $containerdn The distinguished name of the container for the objects to remove.
 444       * @param array $testobjects Array of the tests objects to create. The structure of
 445       *              the array elements *must* follow the structure of the value returned
 446       *              by ldap_get_entries_moodle_test_objects() member function.
 447       *
 448       */
 449      protected function remove_test_objects($connection, $containerdn, $testobjects) {
 450          foreach ($testobjects as $object) {
 451              if ($object['addremove'] !== true) {
 452                  continue;
 453              }
 454              $dn = $this->get_object_dn($object, $containerdn);
 455              ldap_delete($connection, $dn);
 456          }
 457      }
 458  
 459      /**
 460       * Get the distinguished name (DN) for a given object.
 461       *
 462       * @param object $object The LDAP object to calculate the DN for.
 463       * @param string $containerdn The DN of the container to use for objects with relative DNs.
 464       *
 465       * @return string The calculated DN.
 466       */
 467      protected function get_object_dn($object, $containerdn) {
 468          if ($object['relativedn']) {
 469              $dn = $object['dn'].','.$containerdn;
 470          } else {
 471              $dn = $object['dn'];
 472          }
 473          return $dn;
 474      }
 475  }