Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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