/** * Returns the win32 AD epoch number of days the password may be unchanged. * * @return integer|boolean Number of days or false if no limit. */ protected function _getMaxPasswd() { $dn = Horde_Ldap_Util::explodeDN($this->_params['basedn']); $domaindn = array(); foreach ($dn as $rdn) { $attribute = Horde_Ldap_Util::splitAttributeString($rdn); if ($attribute[0] == 'DC') { $domaindn[] = $rdn; } } $dn = Horde_Ldap_Util::canonicalDN($domaindn); $search = $this->_ldap->search($domaindn, 'objectClass=*'); $entry = $search->shiftEntry(); try { return $entry->getValue('maxPwdAge', 'single'); } catch (Horde_Ldap_Exception $e) { return false; } }
/** * Returns whether a DN exists in the directory. * * @param string|Horde_Ldap_Entry $dn The DN of the object to test. * * @return boolean True if the DN exists. * @throws Horde_Ldap_Exception */ public function exists($dn) { if ($dn instanceof Horde_Ldap_Entry) { $dn = $dn->dn(); } if (!is_string($dn)) { throw new Horde_Ldap_Exception('Parameter $dn is not a string nor an entry object!'); } /* Make dn relative to parent. */ $base = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false)); $entry_rdn = array_shift($base); $base = Horde_Ldap_Util::canonicalDN($base); $result = @ldap_list($this->_link, $base, $entry_rdn, array(), 1, 1); if (@ldap_count_entries($this->_link, $result)) { return true; } if ($this->errorName(@ldap_errno($this->_link)) == 'LDAP_NO_SUCH_OBJECT') { return false; } if (@ldap_errno($this->_link)) { throw new Horde_Ldap_Exception(@ldap_error($this->_link), @ldap_errno($this->_link)); } return false; }
/** * Updates the entry on the directory server. * * This will evaluate all changes made so far and send them to the * directory server. * * If you make changes to objectclasses wich have mandatory attributes set, * update() will currently fail. Remove the entry from the server and readd * it as new in such cases. This also will deal with problems with setting * structural object classes. * * @todo Entry rename with a DN containing special characters needs testing! * * @throws Horde_Ldap_Exception */ public function update() { /* Ensure we have a valid LDAP object. */ $ldap = $this->getLDAP(); /* Get and check link. */ $link = $ldap->getLink(); if (!is_resource($link)) { throw new Horde_Ldap_Exception('Could not update entry: internal LDAP link is invalid'); } /* Delete the entry. */ if ($this->_delete) { return $ldap->delete($this); } /* New entry. */ if ($this->_new) { $ldap->add($this); $this->_new = false; $this->_changes['add'] = array(); $this->_changes['delete'] = array(); $this->_changes['replace'] = array(); $this->_original = $this->_attributes; return; } /* Rename/move entry. */ if (!is_null($this->_newdn)) { if ($ldap->getVersion() != 3) { throw new Horde_Ldap_Exception('Renaming/Moving an entry is only supported in LDAPv3'); } /* Make DN relative to parent (needed for LDAP rename). */ $parent = Horde_Ldap_Util::explodeDN($this->_newdn, array('casefolding' => 'none', 'reverse' => false, 'onlyvalues' => false)); $child = array_shift($parent); /* Maybe the DN consist of a multivalued RDN, we must build the DN * in this case because the $child RDN is an array. */ if (is_array($child)) { $child = Horde_Ldap_Util::canonicalDN($child); } $parent = Horde_Ldap_Util::canonicalDN($parent); /* Rename/move. */ if (!@ldap_rename($link, $this->_dn, $child, $parent, true)) { throw new Horde_Ldap_Exception('Entry not renamed: ' . @ldap_error($link), @ldap_errno($link)); } /* Reflect changes to local copy. */ $this->_dn = $this->_newdn; $this->_newdn = null; } /* Carry out modifications to the entry. */ foreach ($this->_changes['add'] as $attr => $value) { /* If attribute exists, add new values. */ if ($this->exists($attr)) { if (!@ldap_mod_add($link, $this->dn(), array($attr => $value))) { throw new Horde_Ldap_Exception('Could not add new values to attribute ' . $attr . ': ' . @ldap_error($link), @ldap_errno($link)); } } else { /* New attribute. */ if (!@ldap_modify($link, $this->dn(), array($attr => $value))) { throw new Horde_Ldap_Exception('Could not add new attribute ' . $attr . ': ' . @ldap_error($link), @ldap_errno($link)); } } unset($this->_changes['add'][$attr]); } foreach ($this->_changes['delete'] as $attr => $value) { /* In LDAPv3 you need to specify the old values for deleting. */ if (is_null($value) && $ldap->getVersion() == 3) { $value = $this->_original[$attr]; } if (!@ldap_mod_del($link, $this->dn(), array($attr => $value))) { throw new Horde_Ldap_Exception('Could not delete attribute ' . $attr . ': ' . @ldap_error($link), @ldap_errno($link)); } unset($this->_changes['delete'][$attr]); } foreach ($this->_changes['replace'] as $attr => $value) { if (!@ldap_modify($link, $this->dn(), array($attr => $value))) { throw new Horde_Ldap_Exception('Could not replace attribute ' . $attr . ' values: ' . @ldap_error($link), @ldap_errno($link)); } unset($this->_changes['replace'][$attr]); } /* All went well, so $_attributes (local copy) becomes $_original * (server). */ $this->_original = $this->_attributes; }
/** * Test group fetching. * * @return NULL */ public function testGetGroups() { foreach ($this->servers as $server) { $filter = '(&(objectClass=kolabGroupOfNames)(member=' . Horde_Ldap_Util::escapeFilterValue('cn=The Administrator,dc=example,dc=org') . '))'; $result = $server->search($filter, array()); $this->assertTrue(!empty($result)); /* $entry = $server->_firstEntry($result); */ /* $this->assertTrue(!empty($entry)); */ /* $uid = $server->_getDn($entry); */ /* $this->assertTrue(!empty($uid)); */ /* $entry = $server->_nextEntry($entry); */ /* $this->assertTrue(empty($entry)); */ /* $entries = $server->_getDns($result); */ /* $this->assertTrue(!empty($entries)); */ $groups = $server->getGroups('cn=The Administrator,dc=example,dc=org'); $this->assertTrue(!empty($groups)); $groups = $server->getGroups($server->uidForIdOrMailOrAlias('*****@*****.**')); $this->assertContains('cn=group@example.org,dc=example,dc=org', $groups); $groups = $server->getGroupAddresses($server->uidForIdOrMailOrAlias('*****@*****.**')); $this->assertContains('*****@*****.**', $groups); $groups = $server->getGroups($server->uidForIdOrMailOrAlias('*****@*****.**')); $this->assertContains('cn=group@example.org,dc=example,dc=org', $groups); $groups = $server->getGroupAddresses($server->uidForIdOrMailOrAlias('*****@*****.**')); $this->assertContains('*****@*****.**', $groups); $groups = $server->getGroups('nobody'); $this->assertTrue(empty($groups)); } }
/** * Get the parent GUID of this object. * * @param string $guid The GUID of the child. * * @return string the parent GUID of this object. */ public function getParentGuid($guid) { try { $base = Horde_Ldap_Util::explodeDN($guid, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false)); $id = array_shift($base); $parent = Horde_Ldap_Util::canonicalDN($base, array('casefold' => 'none')); } catch (Horde_Ldap_Exception $e) { throw new Horde_Kolab_Server_Exception('Retrieving the parent object failed!', Horde_Kolab_Server_Exception::SYSTEM, $e); } return $parent; }
/** * Returns whether a DN exists in the directory. * * @param string|Horde_Ldap_Entry $dn The DN of the object to test. * * @return boolean True if the DN exists. * @throws Horde_Ldap_Exception */ public function exists($dn) { if ($dn instanceof Horde_Ldap_Entry) { $dn = $dn->dn(); } if (!is_string($dn)) { throw new Horde_Ldap_Exception('Parameter $dn is not a string nor an entry object!'); } /* Make dn relative to parent. */ $options = array('casefold' => 'none'); $base = Horde_Ldap_Util::explodeDN($dn, $options); $entry_rdn = '(&(' . Horde_Ldap_Util::canonicalDN(array_shift($base), array_merge($options, array('separator' => ')('))) . '))'; $base = Horde_Ldap_Util::canonicalDN($base, $options); $result = @ldap_list($this->_link, $base, $entry_rdn, array('dn'), 1, 1); if ($result && @ldap_count_entries($this->_link, $result)) { return true; } if ($this->errorName(@ldap_errno($this->_link)) == 'LDAP_NO_SUCH_OBJECT') { return false; } if (@ldap_errno($this->_link)) { throw new Horde_Ldap_Exception(@ldap_error($this->_link), @ldap_errno($this->_link)); } return false; }
/** * Creates a new part of an LDAP filter. * * The following matching rules exists: * - equals: One of the attributes values is exactly $value. * Please note that case sensitiviness depends on the * attributes syntax configured in the server. * - begins: One of the attributes values must begin with $value. * - ends: One of the attributes values must end with $value. * - contains: One of the attributes values must contain $value. * - present | any: The attribute can contain any value but must exist. * - greater: The attributes value is greater than $value. * - less: The attributes value is less than $value. * - greaterOrEqual: The attributes value is greater or equal than $value. * - lessOrEqual: The attributes value is less or equal than $value. * - approx: One of the attributes values is similar to $value. * * If $escape is set to true then $value will be escaped. If set to false * then $value will be treaten as a raw filter value string. You should * then escape it yourself using {@link * Horde_Ldap_Util::escapeFilterValue()}. * * Examples: * <code> * // This will find entries that contain an attribute "sn" that ends with * // "foobar": * $filter = Horde_Ldap_Filter::create('sn', 'ends', 'foobar'); * * // This will find entries that contain an attribute "sn" that has any * // value set: * $filter = Horde_Ldap_Filter::create('sn', 'any'); * </code> * * @param string $attribute Name of the attribute the filter should apply * to. * @param string $match Matching rule (equals, begins, ends, contains, * greater, less, greaterOrEqual, lessOrEqual, * approx, any). * @param string $value If given, then this is used as a filter value. * @param boolean $escape Should $value be escaped? * * @return Horde_Ldap_Filter * @throws Horde_Ldap_Exception */ public static function create($attribute, $match, $value = '', $escape = true) { if ($escape) { $array = Horde_Ldap_Util::escapeFilterValue(array($value)); $value = $array[0]; } switch (Horde_String::lower($match)) { case 'equals': case '=': $filter = '(' . $attribute . '=' . $value . ')'; break; case 'begins': $filter = '(' . $attribute . '=' . $value . '*)'; break; case 'ends': $filter = '(' . $attribute . '=*' . $value . ')'; break; case 'contains': $filter = '(' . $attribute . '=*' . $value . '*)'; break; case 'greater': case '>': $filter = '(' . $attribute . '>' . $value . ')'; break; case 'less': case '<': $filter = '(' . $attribute . '<' . $value . ')'; break; case 'greaterorequal': case '>=': $filter = '(' . $attribute . '>=' . $value . ')'; break; case 'lessorequal': case '<=': $filter = '(' . $attribute . '<=' . $value . ')'; break; case 'approx': case '~=': $filter = '(' . $attribute . '~=' . $value . ')'; break; case 'any': case 'present': $filter = '(' . $attribute . '=*)'; break; default: throw new Horde_Ldap_Exception('Matching rule "' . $match . '" unknown'); } return new Horde_Ldap_Filter(array('filter' => $filter)); }
/** * Tests if canonicalDN() works. * * Note: This tests depend on the default options of canonicalDN(). */ public function testCanonicalDN() { // Test empty dn (is valid according to RFC). $this->assertEquals('', Horde_Ldap_Util::canonicalDN('')); // Default options with common DN. $testdn = 'cn=beni,DC=php,c=net'; $expected = 'CN=beni,DC=php,C=net'; $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn)); // Casefold tests with common DN. $expected_up = 'CN=beni,DC=php,C=net'; $expected_lo = 'cn=beni,dc=php,c=net'; $expected_no = 'cn=beni,DC=php,c=net'; $this->assertEquals($expected_up, Horde_Ldap_Util::canonicalDN($testdn, array('casefold' => 'upper'))); $this->assertEquals($expected_lo, Horde_Ldap_Util::canonicalDN($testdn, array('casefold' => 'lower'))); $this->assertEquals($expected_no, Horde_Ldap_Util::canonicalDN($testdn, array('casefold' => 'none'))); // Reverse. $expected_rev = 'C=net,DC=php,CN=beni'; $this->assertEquals($expected_rev, Horde_Ldap_Util::canonicalDN($testdn, array('reverse' => true)), 'Option reverse failed'); // DN as arrays. $dn_index = array('cn=beni', 'dc=php', 'c=net'); $dn_assoc = array('cn' => 'beni', 'dc' => 'php', 'c' => 'net'); $expected = 'CN=beni,DC=php,C=net'; $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($dn_index)); $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($dn_assoc)); // DN with multiple RDN value. $testdn = 'ou=dev+cn=beni,DC=php,c=net'; $testdn_index = array(array('ou=dev', 'cn=beni'), 'DC=php', 'c=net'); $testdn_assoc = array(array('ou' => 'dev', 'cn' => 'beni'), 'DC' => 'php', 'c' => 'net'); $expected = 'CN=beni+OU=dev,DC=php,C=net'; $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn)); $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn_assoc)); $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($expected)); // Test DN with OID. $testdn = 'OID.2.5.4.3=beni,dc=php,c=net'; $expected = '2.5.4.3=beni,DC=php,C=net'; $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn)); // Test with leading and ending spaces. $testdn = 'cn= beni ,DC=php,c=net'; $expected = 'CN=\\20\\20beni\\20\\20,DC=php,C=net'; $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn)); // Test with escaped commas. Doesn't work at the moment because // canonicalDN() escapes attribute values, which break if they are // already escaped. $testdn = 'cn=beni\\,hi\\=ll,DC=php,c=net'; $expected = 'CN=beni\\,hi\\=ll,DC=php,C=net'; // $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn)); // Test with to-be escaped characters in attribute value. $specialchars = array(',' => '\\,', '+' => '\\+', '"' => '\\"', '\\' => '\\\\', '<' => '\\<', '>' => '\\>', ';' => '\\;', '#' => '\\#', '=' => '\\=', chr(18) => '\\12', '/' => '\\/'); foreach ($specialchars as $char => $escape) { $test_string = 'CN=be' . $char . 'ni,DC=ph' . $char . 'p,C=net'; $test_index = array('CN=be' . $char . 'ni', 'DC=ph' . $char . 'p', 'C=net'); $test_assoc = array('CN' => 'be' . $char . 'ni', 'DC' => 'ph' . $char . 'p', 'C' => 'net'); $expected = 'CN=be' . $escape . 'ni,DC=ph' . $escape . 'p,C=net'; $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($test_string), 'String escaping test (' . $char . ') failed'); $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($test_index), 'Indexed array escaping test (' . $char . ') failed'); $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($test_assoc), 'Associative array encoding test (' . $char . ') failed'); } }
/** * Writes a DN to the file handle. * * @param string $dn DN to write. * * @throws Horde_Ldap_Exception */ protected function _writeDN($dn) { // Prepare DN. if ($this->_options['encode'] == 'base64') { $dn = $this->_convertDN($dn); } elseif ($this->_options['encode'] == 'canonical') { $dn = Horde_Ldap_Util::canonicalDN($dn, array('casefold' => 'none')); } $this->_writeLine($dn, 'Unable to write DN of entry ' . $this->_entrynum); }
/** * Parse LDAP filter. * Partially derived from Net_LDAP_Filter. * * @param string $filter The filter string. * * @return array An array of the parsed filter. * * @throws Horde_Kolab_Server_Exception If parsing the filter expression * fails. */ public function parse($filter) { $result = array(); if (preg_match('/^\\((.+?)\\)$/', $filter, $matches)) { if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) { $result['op'] = substr($matches[1], 0, 1); $result['sub'] = $this->parseSub(substr($matches[1], 1)); return $result; } else { if (stristr($matches[1], ')(')) { throw new Horde_Kolab_Server_Exception('Filter parsing error: invalid filter syntax - multiple leaf components detected!'); } else { $filter_parts = preg_split('/(?<!\\\\)(=|=~|>|<|>=|<=)/', $matches[1], 2, PREG_SPLIT_DELIM_CAPTURE); if (count($filter_parts) != 3) { throw new Horde_Kolab_Server_Exception('Filter parsing error: invalid filter syntax - unknown matching rule used'); } else { $result['att'] = $filter_parts[0]; $result['log'] = $filter_parts[1]; $val = Horde_Ldap_Util::unescapeFilterValue($filter_parts[2]); $result['val'] = $val[0]; return $result; } } } } else { throw new Horde_Kolab_Server_Exception(sprintf("Filter parsing error: %s - filter components must be enclosed in round brackets", $filter)); } }
/** * Test adding a person with two common names. * * @return NULL */ public function testAddDoubleCnPerson() { foreach ($this->servers as $server) { $person = $this->assertAdd($server, $this->objects[5], array()); $cn_result = $server->uidForCn($this->objects[5]['Cn'][0]); $this->assertNoError($cn_result); $dn_parts = Horde_Ldap_Util::explodeDN($cn_result, array('casefold' => 'lower')); $dnpart = Horde_Ldap_Util::unescapeDNValue($dn_parts[0]); $this->assertContains('Cn' . '=' . $this->objects[5]['Cn'][0], $dnpart[0]); } }
/** * Returns a list of users in a group. * * @param mixed $gid A group ID. * * @return array List of group users. * @throws Horde_Group_Exception * @throws Horde_Exception_NotFound */ public function listUsers($gid) { $attr = $this->_params['memberuid']; try { $entry = $this->_ldap->getEntry($gid, array($attr)); if (!$entry->exists($attr)) { return array(); } if (empty($this->_params['attrisdn'])) { return $entry->getValue($attr, 'all'); } $users = array(); foreach ($entry->getValue($attr, 'all') as $user) { $dn = Horde_Ldap_Util::explodeDN($user, array('onlyvalues' => true)); // Very simplified approach: assume the first element of the DN // contains the user ID. $user = $dn[0]; // Check for multi-value RDNs. if (is_array($element)) { $user = $element[0]; } $users[] = $user; } return $users; } catch (Horde_Ldap_Exception $e) { throw new Horde_Group_Exception($e); } }