package.xml0000664000175000017500000004527113054777070011322 0ustar janjan Horde_Ldap pear.horde.org Horde_LDAP Horde LDAP libraries A set of classes for connecting to LDAP servers and working with directory objects. Ben Klang bklang ben@alkaloid.net yes Jan Schneider jan jan@horde.org yes 2017-02-27 2.4.0 1.4.0 stable stable LGPL-3.0 * [jan] Optionally cache rootDSE requests too. * [jan] Fix checking for existing attributes in Horde_Ldap_RootDse. 5.3.0 8.0.0alpha1 8.0.0alpha1 1.7.0 Horde_Exception pear.horde.org 2.0.0 3.0.0alpha1 3.0.0alpha1 Horde_Util pear.horde.org 2.0.0 3.0.0alpha1 3.0.0alpha1 ldap Horde_Test pear.horde.org 2.1.0 3.0.0alpha1 3.0.0alpha1 1.0.0alpha1 1.0.0 alpha alpha 2011-03-08 LGPLv3 * First alpha release for Horde 4. * Forked from PEAR-Net_LDAP2-2.0.7. * Converted to PHP5 strict standards. * Converted to use exceptions. * Included functions from the previous Horde_LDAP for backwards compatibility. 1.0.0beta1 1.0.0 beta beta 2011-03-16 LGPL-3.0 * First beta release for Horde 4. 1.0.0RC1 1.0.0 beta beta 2011-03-22 LGPL-3.0 * First release candidate for Horde 4. 1.0.0RC2 1.0.0 beta beta 2011-03-29 LGPL-3.0 * Second release candidate for Horde 4. 1.0.0 1.0.0 stable stable 2011-04-06 LGPL-3.0 * First stable release for Horde 4. 1.1.0 1.1.0 stable stable 2011-04-20 LGPL-3.0 * [jan] Add parameter to Horde_Ldap_Schema#must() and #may() to return attributes from superior objectclasses too (Bug #9826). * [jan] Don't throw exceptions from Horde_Ldap_Schema#must() and #may() (Bug #9826). 1.1.1 1.1.0 stable stable 2011-05-03 LGPL-3.0 * [jan] Fix detecting superiour objectclass' must/may attributes (Bug #9826). 1.1.2 1.1.0 stable stable 2011-07-05 LGPL-3.0 * [jan] Always use negative lookbehind assertions to work around bug in PCRE 6.6 (Steve Teti, Bug #10294). 1.1.3 1.1.0 stable stable 2011-08-30 LGPL-3.0 * [jan] Speed up Horde_Ldap_Search#shiftEntry() on large result sets. 1.1.4 1.1.0 stable stable 2011-11-08 LGPL-3.0 * [jan] Add missing test autoloader. 1.1.5 1.1.0 stable stable 2012-05-01 LGPL-3.0 * [jan] Throw exception if LDAP extension is missing. 2.0.0alpha1 1.1.0 alpha stable 2012-07-05 LGPL-3.0 * First alpha release for Horde 5. 2.0.0beta1 1.1.0 beta stable 2012-07-19 LGPL-3.0 * First beta release for Horde 5. 2.0.0 1.1.0 stable stable 2012-10-30 LGPL-3.0 * First stable release for Horde 5. 2.0.1 1.1.0 stable stable 2012-11-19 LGPL-3.0 * [mms] Use new Horde_Test layout. 2.0.2 1.1.0 stable stable 2013-01-29 LGPL-3.0 * [jan] Replace preg_replace() /e modifier. 2.0.3 1.1.0 stable stable 2013-10-28 LGPL-3.0 * [jan] Fix modifying entries with modify(). * [jan] Try starting TLS without querying the rootDSE (Bug #12157). 2.0.4 1.1.0 stable stable 2014-04-03 LGPL-3.0 * [jan] Fix approximate search operator (Leandro Damascena <leandro.damascena@gmail.com>, Bug #9094). 2.0.5 1.1.0 stable stable 2014-05-21 LGPL-3.0 * [jan] Fix creating filters with the less (<) operator (Bug #13154). 2.0.6 1.1.0 stable stable 2014-06-03 LGPL-3.0 * [jan] SECURITY: Stricter parameter check in bind() to detect empty passwords. 2.1.0 1.1.1 stable stable 2014-06-10 LGPL-3.0 * [jan] Support multi-value RDNs in Horde_Ldap::quoteDN() (Request #11888). 2.2.0 1.2.0 stable stable 2014-06-17 LGPL-3.0 * [jan] Fix compatibility with PHP 5.3 (Bug #11888). * [jan] Allow to specify base DN for searching user DNs. 2.3.0 1.3.0 stable stable 2015-02-10 LGPL-3.0 * [jan] Fix error when using custom separators in Horde_Ldap_Utils::canonicalDN(). * [jan] Fix casefolding option not being passed to multivalued RDNs. * [jan] Fix exists() with multivalued RDNs. * [jan] Add 'timeout' parameter. 2.3.1 1.3.0 stable stable 2015-02-12 LGPL-3.0 * [jan] Fix connection if using ldaps:// scheme (Bug #13858). 2.3.2 1.3.0 stable stable 2016-02-01 LGPL-3.0 * [jan] Mark PHP 7 as supported. 2.4.0 1.4.0 stable stable 2017-02-27 LGPL-3.0 * [jan] Optionally cache rootDSE requests too. * [jan] Fix checking for existing attributes in Horde_Ldap_RootDse. Horde_Ldap-2.4.0/doc/Horde/Ldap/COPYING0000664000175000017500000001674313054777070015370 0ustar janjan GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. Horde_Ldap-2.4.0/doc/horde.yml0000664000175000017500000000024313054777070014224 0ustar janjan--- id: Ldap name: Ldap full: LDAP client library description: > A set of classes for connecting to LDAP servers and working with directory objects. list: dev Horde_Ldap-2.4.0/lib/Horde/Ldap/Entry.php0000664000175000017500000007373713054777070016156 0ustar janjan * @author Tarjej Huse * @author Benedikt Hallinger * @author Ben Klang * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_Entry { /** * Entry resource identifier. * * @var resource */ protected $_entry; /** * LDAP resource identifier. * * @var resource */ protected $_link; /** * Horde_Ldap object. * * This object will be used for updating and schema checking. * * @var Horde_Ldap */ protected $_ldap; /** * Distinguished name of the entry. * * @var string */ protected $_dn; /** * Attributes. * * @var array */ protected $_attributes = array(); /** * Original attributes before any modification. * * @var array */ protected $_original = array(); /** * Map of attribute names. * * @var array */ protected $_map = array(); /** * Is this a new entry? * * @var boolean */ protected $_new = true; /** * New distinguished name. * * @var string */ protected $_newdn; /** * Shall the entry be deleted? * * @var boolean */ protected $_delete = false; /** * Map with changes to the entry. * * @var array */ protected $_changes = array('add' => array(), 'delete' => array(), 'replace' => array()); /** * Constructor. * * Sets up the distinguished name and the entries attributes. * * Use {@link Horde_Ldap_Entry::createFresh()} or {@link * Horde_Ldap_Entry::createConnected()} to create Horde_Ldap_Entry objects. * * @param Horde_Ldap|resource|array $ldap Horde_Ldap object, LDAP * connection resource or * array of attributes. * @param string|resource $entry Either a DN or a LDAP entry * resource. */ protected function __construct($ldap, $entry = null) { /* Set up entry resource or DN. */ if (is_resource($entry)) { $this->_entry = $entry; } else { $this->_dn = $entry; } /* Set up LDAP link. */ if ($ldap instanceof Horde_Ldap) { $this->_ldap = $ldap; $this->_link = $ldap->getLink(); } elseif (is_resource($ldap)) { $this->_link = $ldap; } elseif (is_array($ldap)) { /* Special case: here $ldap is an array of attributes, this means, * we have no link. This is a "virtual" entry. We just set up the * attributes so one can work with the object as expected, but an * update() fails unless setLDAP() is called. */ $this->_loadAttributes($ldap); } /* If this is an entry existing in the directory, then set up as old * and fetch attributes. */ if (is_resource($this->_entry) && is_resource($this->_link)) { $this->_new = false; $this->_dn = @ldap_get_dn($this->_link, $this->_entry); /* Fetch attributes from server. */ $this->_loadAttributes(); } } /** * Creates a fresh entry that may be added to the directory later. * * You should put a 'objectClass' attribute into the $attrs so the * directory server knows which object you want to create. However, you may * omit this in case you don't want to add this entry to a directory * server. * * The attributes parameter is as following: * * $attrs = array('attribute1' => array('value1', 'value2'), * 'attribute2' => 'single value'); * * * @param string $dn DN of the entry. * @param array $attrs Attributes of the entry. * * @return Horde_Ldap_Entry * @throws Horde_Ldap_Exception */ public static function createFresh($dn, array $attrs = array()) { return new Horde_Ldap_Entry($attrs, $dn); } /** * Creates an entry object out of an LDAP entry resource. * * Use this method, if you want to initialize an entry object that is * already present in some directory and that you have read manually. * * @param Horde_Ldap $ldap Horde_Ldap object. * @param resource $entry PHP LDAP entry resource. * * @return Horde_Ldap_Entry * @throws Horde_Ldap_Exception */ public static function createConnected(Horde_Ldap $ldap, $entry) { if (!is_resource($entry)) { throw new Horde_Ldap_Exception('Unable to create connected entry: Parameter $entry needs to be a ldap entry resource!'); } return new Horde_Ldap_Entry($ldap, $entry); } /** * Creates an entry object that is considered to exist already. * * Use this method, if you want to modify an already existing entry without * fetching it first. In most cases however, it is better to fetch the * entry via Horde_Ldap::getEntry(). * * You should take care if you construct entries manually with this because * you may get weird synchronisation problems. The attributes and values * as well as the entry itself are considered existent which may produce * errors if you try to modify an entry which doesn't really exist or if * you try to overwrite some attribute with an value already present. * * The attributes parameter is as following: * * $attrs = array('attribute1' => array('value1', 'value2'), * 'attribute2' => 'single value'); * * * @param string $dn DN of the entry. * @param array $attrs Attributes of the entry. * * @return Horde_Ldap_Entry * @throws Horde_Ldap_Exception */ public static function createExisting($dn, array $attrs = array()) { $entry = self::createFresh($dn, $attrs); $entry->markAsNew(false); return $entry; } /** * Returns or sets the distinguished name of the entry. * * If called without an argument the current (or the new DN if set) DN gets * returned. * * If you provide an DN, this entry is moved to the new location specified * if a DN existed. * * If the DN was not set, the DN gets initialized. Call {@link update()} to * actually create the new entry in the directory. * * To fetch the current active DN after setting a new DN but before an * update(), you can use {@link currentDN()} to retrieve the DN that is * currently active. * * @todo expect utf-8 data. * Please note that special characters (eg german umlauts) should be encoded using utf8_encode(). * You may use {@link Horde_Ldap_Util::canonicalDN()} for properly encoding of the DN. * * @param string $dn New distinguished name. * * @return string Distinguished name. */ public function dn($dn = null) { if (!is_null($dn)) { if (is_null($this->_dn)) { $this->_dn = $dn; } else { $this->_newdn = $dn; } return $dn; } return isset($this->_newdn) ? $this->_newdn : $this->currentDN(); } /** * Sets the internal attributes array. * * This method fetches the values for the attributes from the server. The * attribute syntax will be checked so binary attributes will be returned * as binary values. * * Attributes may be passed directly via the $attributes parameter to setup * this entry manually. This overrides attribute fetching from the server. * * @param array $attributes Attributes to set for this entry. */ protected function _loadAttributes(array $attributes = null) { /* Fetch attributes from the server. */ if (is_null($attributes) && is_resource($this->_entry) && is_resource($this->_link)) { /* Fetch schema. */ if ($this->_ldap instanceof Horde_Ldap) { try { $schema = $this->_ldap->schema(); } catch (Horde_Ldap_Exception $e) { $schema = null; } } /* Fetch attributes. */ $attributes = array(); for ($attr = @ldap_first_attribute($this->_link, $this->_entry); $attr; $attr = @ldap_next_attribute($this->_link, $this->_entry)) { /* Standard function to fetch value. */ $func = 'ldap_get_values'; /* Try to get binary values as binary data. */ if ($schema instanceof Horde_Ldap_Schema && $schema->isBinary($attr)) { $func = 'ldap_get_values_len'; } /* Fetch attribute value (needs error checking?) . */ $attributes[$attr] = $func($this->_link, $this->_entry, $attr); } } /* Set attribute data directly, if passed. */ if (is_array($attributes) && count($attributes) > 0) { if (isset($attributes['count']) && is_numeric($attributes['count'])) { unset($attributes['count']); } foreach ($attributes as $k => $v) { /* Attribute names should not be numeric. */ if (is_numeric($k)) { continue; } /* Map generic attribute name to real one. */ $this->_map[Horde_String::lower($k)] = $k; /* Attribute values should be in an array. */ if (false == is_array($v)) { $v = array($v); } /* Remove the value count (comes from LDAP server). */ if (isset($v['count'])) { unset($v['count']); } $this->_attributes[$k] = $v; } } /* Save a copy for later use. */ $this->_original = $this->_attributes; } /** * Returns the values of all attributes in a hash. * * The returned hash has the form * * array('attributename' => 'single value', * 'attributename' => array('value1', value2', value3')) * * * @return array Hash of all attributes with their values. * @throws Horde_Ldap_Exception */ public function getValues() { $attrs = array(); foreach (array_keys($this->_attributes) as $attr) { $attrs[$attr] = $this->getValue($attr); } return $attrs; } /** * Returns the value of a specific attribute. * * The first parameter is the name of the attribute. * * The second parameter influences the way the value is returned: * - 'single': only the first value is returned as string. * - 'all': all values are returned in an array. * In all other cases an attribute value with a single value is returned as * string, if it has multiple values it is returned as an array. * * @param string $attr Attribute name. * @param string $option Option. * * @return string|array Attribute value(s). * @throws Horde_Ldap_Exception */ public function getValue($attr, $option = null) { $attr = $this->_getAttrName($attr); if (!array_key_exists($attr, $this->_attributes)) { throw new Horde_Ldap_Exception('Unknown attribute (' . $attr . ') requested'); } $value = $this->_attributes[$attr]; if ($option == 'single' || (count($value) == 1 && $option != 'all')) { $value = array_shift($value); } return $value; } /** * Returns an array of attributes names. * * @return array Array of attribute names. */ public function attributes() { return array_keys($this->_attributes); } /** * Returns whether an attribute exists or not. * * @param string $attr Attribute name. * * @return boolean True if the attribute exists. */ public function exists($attr) { $attr = $this->_getAttrName($attr); return array_key_exists($attr, $this->_attributes); } /** * Adds new attributes or a new values to existing attributes. * * The paramter has to be an array of the form: * * array('attributename' => 'single value', * 'attributename' => array('value1', 'value2')) * * * When the attribute already exists the values will be added, otherwise * the attribute will be created. These changes are local to the entry and * do not affect the entry on the server until update() is called. * * You can add values of attributes that you haven't originally selected, * but if you do so, {@link getValue()} and {@link getValues()} will only * return the values you added, *NOT* all values present on the server. To * avoid this, just refetch the entry after calling {@link update()} or * select the attribute. * * @param array $attr Attributes to add. */ public function add(array $attr = array()) { foreach ($attr as $k => $v) { $k = $this->_getAttrName($k); if (!is_array($v)) { /* Do not add empty values. */ if ($v == null) { continue; } else { $v = array($v); } } /* Add new values to existing attribute or add new attribute. */ if ($this->exists($k)) { $this->_attributes[$k] = array_unique(array_merge($this->_attributes[$k], $v)); } else { $this->_map[Horde_String::lower($k)] = $k; $this->_attributes[$k] = $v; } /* Save changes for update(). */ if (empty($this->_changes['add'][$k])) { $this->_changes['add'][$k] = array(); } $this->_changes['add'][$k] = array_unique(array_merge($this->_changes['add'][$k], $v)); } } /** * Deletes an attribute, a value or the whole entry. * * The parameter can be one of the following: * * - 'attributename': the attribute as a whole will be deleted. * - array('attributename1', 'attributename2'): all specified attributes * will be deleted. * - array('attributename' => 'value'): the specified attribute value will * be deleted. * - array('attributename' => array('value1', 'value2'): The specified * attribute values * will be deleted. * - null: the whole entry will be deleted. * * These changes are local to the entry and do not affect the entry on the * server until {@link update()} is called. * * You must select the attribute (at $ldap->search() for example) to be * able to delete values of it, Otherwise {@link update()} will silently * fail and remove nothing. * * @param string|array $attr Attributes to delete. */ public function delete($attr = null) { if (is_null($attr)) { $this->_delete = true; return; } if (is_string($attr)) { $attr = array($attr); } /* Make the assumption that attribute names cannot be numeric, * therefore this has to be a simple list of attribute names to * delete. */ reset($attr); if (is_numeric(key($attr))) { foreach ($attr as $name) { if (is_array($name)) { /* Mixed modes (list mode but specific values given!). */ $del_attr_name = array_search($name, $attr); $this->delete(array($del_attr_name => $name)); } else { /* Mark for update() if this attribute was not marked before. */ $name = $this->_getAttrName($name); if ($this->exists($name)) { $this->_changes['delete'][$name] = null; unset($this->_attributes[$name]); } } } } else { /* We have a hash with 'attributename' => 'value to delete'. */ foreach ($attr as $name => $values) { if (is_int($name)) { /* Mixed modes and gave us just an attribute name. */ $this->delete($values); } else { /* Mark for update() if this attribute was not marked * before; this time it must consider the selected values * too. */ $name = $this->_getAttrName($name); if ($this->exists($name)) { if (!is_array($values)) { $values = array($values); } /* Save values to be deleted. */ if (empty($this->_changes['delete'][$name])) { $this->_changes['delete'][$name] = array(); } $this->_changes['delete'][$name] = array_unique(array_merge($this->_changes['delete'][$name], $values)); foreach ($values as $value) { /* Find the key for the value that should be * deleted. */ $key = array_search($value, $this->_attributes[$name]); if (false !== $key) { /* Delete the value. */ unset($this->_attributes[$name][$key]); } } } } } } } /** * Replaces attributes or their values. * * The parameter has to an array of the following form: * * array('attributename' => 'single value', * 'attribute2name' => array('value1', 'value2'), * 'deleteme1' => null, * 'deleteme2' => '') * * * If the attribute does not yet exist it will be added instead (see also * $force). If the attribue value is null, the attribute will de deleted. * * These changes are local to the entry and do not affect the entry on the * server until {@link update()} is called. * * In some cases you are not allowed to read the attributes value (for * example the ActiveDirectory attribute unicodePwd) but are allowed to * replace the value. In this case replace() would assume that the * attribute is not in the directory yet and tries to add it which will * result in an LDAP_TYPE_OR_VALUE_EXISTS error. To force replace mode * instead of add, you can set $force to true. * * @param array $attr Attributes to replace. * @param boolean $force Force replacing mode in case we can't read the * attribute value but are allowed to replace it. */ public function replace(array $attr = array(), $force = false) { foreach ($attr as $k => $v) { $k = $this->_getAttrName($k); if (!is_array($v)) { /* Delete attributes with empty values; treat integers as * string. */ if (is_int($v)) { $v = (string)$v; } if ($v == null) { $this->delete($k); continue; } else { $v = array($v); } } /* Existing attributes will get replaced. */ if ($this->exists($k) || $force) { $this->_changes['replace'][$k] = $v; $this->_attributes[$k] = $v; } else { /* New ones just get added. */ $this->add(array($k => $v)); } } } /** * 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; } /** * Returns the right attribute name. * * @param string $attr Name of attribute. * * @return string The right name of the attribute */ protected function _getAttrName($attr) { $name = Horde_String::lower($attr); return isset($this->_map[$name]) ? $this->_map[$name] : $attr; } /** * Returns a reference to the LDAP-Object of this entry. * * @return Horde_Ldap Reference to the Horde_Ldap object (the connection). * @throws Horde_Ldap_Exception */ public function getLDAP() { if (!($this->_ldap instanceof Horde_Ldap)) { throw new Horde_Ldap_Exception('ldap property is not a valid Horde_Ldap object'); } return $this->_ldap; } /** * Sets a reference to the LDAP object of this entry. * * After setting a Horde_Ldap object, calling update() will use that object * for updating directory contents. Use this to dynamicly switch * directories. * * @param Horde_Ldap $ldap Horde_Ldap object that this entry should be * connected to. * * @throws Horde_Ldap_Exception */ public function setLDAP(Horde_Ldap $ldap) { $this->_ldap = $ldap; } /** * Marks the entry as new or existing. * * If an entry is marked as new, it will be added to the directory when * calling {@link update()}. * * If the entry is marked as old ($mark = false), then the entry is assumed * to be present in the directory server wich results in modification when * calling {@link update()}. * * @param boolean $mark Whether to mark the entry as new. */ public function markAsNew($mark = true) { $this->_new = (bool)$mark; } /** * Applies a regular expression onto a single- or multi-valued attribute * (like preg_match()). * * This method behaves like PHP's preg_match() but with some exception. * Since it is possible to have multi valued attributes the $matches * array will have a additionally numerical dimension (one for each value): * * $matches = array( * 0 => array (usual preg_match() returned array), * 1 => array (usual preg_match() returned array) * ) * * $matches will always be initialized to an empty array inside. * * Usage example: * * try { * if ($entry->pregMatch('/089(\d+)/', 'telephoneNumber', $matches)) { * // Match of value 1, content of first bracket * echo 'First match: ' . $matches[0][1]; * } else { * echo 'No match found.'; * } * } catch (Horde_Ldap_Exception $e) { * echo 'Error: ' . $e->getMessage(); * } * * * @param string $regex The regular expression. * @param string $attr_name The attribute to search in. * @param array $matches Array to store matches in. * * @return boolean True if we had a match in one of the values. * @throws Horde_Ldap_Exception */ public function pregMatch($regex, $attr_name, &$matches = array()) { /* Fetch attribute values. */ $attr = $this->getValue($attr_name, 'all'); unset($attr['count']); /* Perform preg_match() on all values. */ $match = false; foreach ($attr as $thisvalue) { if (preg_match($regex, $thisvalue, $matches_int)) { $match = true; $matches[] = $matches_int; } } return $match; } /** * Returns whether the entry is considered new (not present in the server). * * This method doesn't tell you if the entry is really not present on the * server. Use {@link Horde_Ldap::exists()} to see if an entry is already * there. * * @return boolean True if this is considered a new entry. */ public function isNew() { return $this->_new; } /** * Is this entry going to be deleted once update() is called? * * @return boolean True if this entry is going to be deleted. */ public function willBeDeleted() { return $this->_delete; } /** * Is this entry going to be moved once update() is called? * * @return boolean True if this entry is going to be move. */ public function willBeMoved() { return $this->dn() !== $this->currentDN(); } /** * Returns always the original DN. * * If an entry will be moved but {@link update()} was not called, {@link * dn()} will return the new DN. This method however, returns always the * current active DN. * * @return string The current DN */ public function currentDN() { return $this->_dn; } /** * Returns the attribute changes to be carried out once update() is called. * * @return array The due changes. */ public function getChanges() { return $this->_changes; } } Horde_Ldap-2.4.0/lib/Horde/Ldap/Exception.php0000664000175000017500000000064413054777070016776 0ustar janjan */ class Horde_Ldap_Exception extends Horde_Exception_Wrapped { } Horde_Ldap-2.4.0/lib/Horde/Ldap/Filter.php0000664000175000017500000004007613054777070016270 0ustar janjan * $filter0 = Horde_Ldap_Filter::create('stars', 'equals', '***'); * $filter_not0 = Horde_Ldap_Filter::combine('not', $filter0); * * $filter1 = Horde_Ldap_Filter::create('gn', 'begins', 'bar'); * $filter2 = Horde_Ldap_Filter::create('gn', 'ends', 'baz'); * $filter_comp = Horde_Ldap_Filter::combine('or', array($filter_not0, $filter1, $filter2)); * * echo (string)$filter_comp; * // This will output: (|(!(stars=\0x5c0x2a\0x5c0x2a\0x5c0x2a))(gn=bar*)(gn=*baz)) * // The stars in $filter0 are treaten as real stars unless you disable escaping. * * * Copyright 2009 Benedikt Hallinger * Copyright 2010-2017 Horde LLC (http://www.horde.org/) * * @category Horde * @package Ldap * @author Benedikt Hallinger * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_Filter { /** * Storage for combination of filters. * * This variable holds a array of filter objects that should be combined by * this filter object. * * @var array */ protected $_filters = array(); /** * Operator for sub-filters. * * @var string */ protected $_operator; /** * Single filter. * * If this is a leaf filter, the filter representation is store here. * * @var string */ protected $_filter; /** * Constructor. * * Construction of Horde_Ldap_Filter objects should happen through either * {@link create()} or {@link combine()} which give you more control. * However, you may use the constructor if you already have generated * filters. * * @param array $params List of object parameters */ protected function __construct(array $params) { foreach ($params as $param => $value) { if (in_array($param, array('filter', 'filters', 'operator'))) { $this->{'_' . $param} = $value; } } } /** * 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: * * // 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'); * * * @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)); } /** * Combines two or more filter objects using a logical operator. * * Example: * * $filter = Horde_Ldap_Filter::combine('or', array($filter1, $filter2)); * * * If the array contains filter strings instead of filter objects, they * will be parsed. * * @param string $operator * The logical operator, either "and", "or", "not" or the logical * equivalents "&", "|", "!". * @param array|Horde_Ldap_Filter|string $filters * Array with Horde_Ldap_Filter objects and/or strings or a single * filter when using the "not" operator. * * @return Horde_Ldap_Filter * @throws Horde_Ldap_Exception */ public static function combine($operator, $filters) { // Substitute named operators with logical operators. switch ($operator) { case 'and': $operator = '&'; break; case 'or': $operator = '|'; break; case 'not': $operator = '!'; break; } // Tests for sane operation. switch ($operator) { case '!': // Not-combination, here we only accept one filter object or filter // string. if ($filters instanceof Horde_Ldap_Filter) { $filters = array($filters); // force array } elseif (is_string($filters)) { $filters = array(self::parse($filters)); } elseif (is_array($filters)) { throw new Horde_Ldap_Exception('Operator is "not" but $filter is an array'); } else { throw new Horde_Ldap_Exception('Operator is "not" but $filter is not a valid Horde_Ldap_Filter nor a filter string'); } break; case '&': case '|': if (!is_array($filters) || count($filters) < 2) { throw new Horde_Ldap_Exception('Parameter $filters is not an array or contains less than two Horde_Ldap_Filter objects'); } break; default: throw new Horde_Ldap_Exception('Logical operator is unknown'); } foreach ($filters as $key => $testfilter) { // Check for errors. if (is_string($testfilter)) { // String found, try to parse into an filter object. $filters[$key] = self::parse($testfilter); } elseif (!($testfilter instanceof Horde_Ldap_Filter)) { throw new Horde_Ldap_Exception('Invalid object passed in array $filters!'); } } return new Horde_Ldap_Filter(array('filters' => $filters, 'operator' => $operator)); } /** * Builds a filter (commonly for objectClass attributes) from different * configuration options. * * @param array $params Hash with configuration options that build the * search filter. Possible hash keys: * - 'filter': An LDAP filter string. * - 'objectclass' (string): An objectClass name. * - 'objectclass' (array): A list of objectClass * names. * @param string $operator How to combine mutliple 'objectclass' entries. * 'and' or 'or'. * * @return Horde_Ldap_Filter A filter matching the specified criteria. * @throws Horde_Ldap_Exception */ public static function build(array $params, $operator = 'and') { if (!empty($params['filter'])) { return self::parse($params['filter']); } if (!is_array($params['objectclass'])) { return self::create('objectclass', 'equals', $params['objectclass']); } $filters = array(); foreach ($params['objectclass'] as $objectclass) { $filters[] = self::create('objectclass', 'equals', $objectclass); } if (count($filters) == 1) { return $filters[0]; } return self::combine($operator, $filters); } /** * Parses a string into a Horde_Ldap_Filter object. * * @todo Leaf-mode: Do we need to escape at all? what about *-chars? Check * for the need of encoding values, tackle problems (see code comments). * * @param string $filter An LDAP filter string. * * @return Horde_Ldap_Filter * @throws Horde_Ldap_Exception */ public static function parse($filter) { if (!preg_match('/^\((.+?)\)$/', $filter, $matches)) { throw new Horde_Ldap_Exception('Invalid filter syntax, filter components must be enclosed in round brackets'); } if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) { return self::_parseCombination($matches[1]); } else { return self::_parseLeaf($matches[1]); } } /** * Parses combined subfilter strings. * * Passes subfilters to parse() and combines the objects using the logical * operator detected. Each subfilter could be an arbitary complex * subfilter. * * @param string $filter An LDAP filter string. * * @return Horde_Ldap_Filter * @throws Horde_Ldap_Exception */ protected static function _parseCombination($filter) { // Extract logical operator and filter arguments. $operator = substr($filter, 0, 1); $filter = substr($filter, 1); // Split $filter into individual subfilters. We cannot use split() for // this, because we do not know the complexiness of the // subfilter. Thus, we look trough the filter string and just recognize // ending filters at the first level. We record the index number of the // char and use that information later to split the string. $sub_index_pos = array(); // Previous character looked at. $prev_char = ''; // Denotes the current bracket level we are, >1 is too deep, 1 is ok, 0 // is outside any subcomponent. $level = 0; for ($curpos = 0, $len = strlen($filter); $curpos < $len; $curpos++) { $cur_char = $filter{$curpos}; // Rise/lower bracket level. if ($cur_char == '(' && $prev_char != '\\') { $level++; } elseif ($cur_char == ')' && $prev_char != '\\') { $level--; } if ($cur_char == '(' && $prev_char == ')' && $level == 1) { // Mark the position for splitting. $sub_index_pos[] = $curpos; } $prev_char = $cur_char; } // Now perform the splits. To get the last part too, we need to add the // "END" index to the split array. $sub_index_pos[] = strlen($filter); $subfilters = array(); $oldpos = 0; foreach ($sub_index_pos as $s_pos) { $str_part = substr($filter, $oldpos, $s_pos - $oldpos); $subfilters[] = $str_part; $oldpos = $s_pos; } if (count($subfilters) > 1) { // Several subfilters found. if ($operator == '!') { throw new Horde_Ldap_Exception('Invalid filter syntax: NOT operator detected but several arguments given'); } } elseif (!count($subfilters)) { // This should not happen unless the user specified a wrong filter. throw new Horde_Ldap_Exception('Invalid filter syntax: got operator ' . $operator . ' but no argument'); } // Now parse the subfilters into objects and combine them using the // operator. $subfilters_o = array(); foreach ($subfilters as $s_s) { $subfilters_o[] = self::parse($s_s); } if (count($subfilters_o) == 1) { $subfilters_o = $subfilters_o[0]; } return self::combine($operator, $subfilters_o); } /** * Parses a single leaf component. * * @param string $filter An LDAP filter string. * * @return Horde_Ldap_Filter * @throws Horde_Ldap_Exception */ protected static function _parseLeaf($filter) { // Detect multiple leaf components. // [TODO] Maybe this will make problems with filters containing // brackets inside the value. if (strpos($filter, ')(')) { throw new Horde_Ldap_Exception('Invalid filter syntax: multiple leaf components detected'); } $filter_parts = preg_split('/(?|<|>=|<=)/', $filter, 2, PREG_SPLIT_DELIM_CAPTURE); if (count($filter_parts) != 3) { throw new Horde_Ldap_Exception('Invalid filter syntax: unknown matching rule used'); } // [TODO]: Do we need to escape at all? what about *-chars user provide // and that should remain special? I think, those prevent // escaping! We need to check against PERL Net::LDAP! // $value_arr = Horde_Ldap_Util::escapeFilterValue(array($filter_parts[2])); // $value = $value_arr[0]; return new Horde_Ldap_Filter(array('filter' => '(' . $filter_parts[0] . $filter_parts[1] . $filter_parts[2] . ')')); } /** * Returns the string representation of this filter. * * This method runs through all filter objects and creates the string * representation of the filter. * * @return string */ public function __toString() { if (!count($this->_filters)) { return $this->_filter; } $return = ''; foreach ($this->_filters as $filter) { $return .= (string)$filter; } return '(' . $this->_operator . $return . ')'; } } Horde_Ldap-2.4.0/lib/Horde/Ldap/Ldif.php0000664000175000017500000007001513054777070015715 0ustar janjan * // Read and parse an LDIF file into Horde_Ldap_Entry objects * // and print out the DNs. Store the entries for later use. * $entries = array(); * $ldif = new Horde_Ldap_Ldif('test.ldif', 'r', $options); * do { * $entry = $ldif->readEntry(); * $dn = $entry->dn(); * echo " done building entry: $dn\n"; * $entries[] = $entry; * } while (!$ldif->eof()); * $ldif->done(); * * // Write those entries to another file * $ldif = new Horde_Ldap_Ldif('test.out.ldif', 'w', $options); * $ldif->writeEntry($entries); * $ldif->done(); * * * Copyright 2009 Benedikt Hallinger * Copyright 2010-2017 Horde LLC (http://www.horde.org/) * * @category Horde * @package Ldap * @author Benedikt Hallinger * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 * @see http://www.ietf.org/rfc/rfc2849.txt * @todo LDAPv3 controls are not implemented yet */ class Horde_Ldap_Ldif { /** * Options. * * @var array */ protected $_options = array('encode' => 'base64', 'change' => false, 'lowercase' => false, 'sort' => false, 'version' => null, 'wrap' => 78, 'raw' => ''); /** * File handle for read/write. * * @var resource */ protected $_fh; /** * Whether we opened the file handle ourselves. * * @var boolean */ protected $_fhOpened = false; /** * Line counter for input file handle. * * @var integer */ protected $_inputLine = 0; /** * Counter for processed entries. * * @var integer */ protected $_entrynum = 0; /** * Mode we are working in. * * Either 'r', 'a' or 'w' * * @var string */ protected $_mode; /** * Whether the LDIF version string was already written. * * @var boolean */ protected $_versionWritten = false; /** * Cache for lines that have built the current entry. * * @var array */ protected $_linesCur = array(); /** * Cache for lines that will build the next entry. * * @var array */ protected $_linesNext = array(); /** * Constructor. * * Opens an LDIF file for reading or writing. * * $options is an associative array and may contain: * - 'encode' (string): Some DN values in LDIF cannot be written verbatim * and have to be encoded in some way. Possible * values: * - 'none': No encoding. * - 'canonical': See {@link * Horde_Ldap_Util::canonicalDN()}. * - 'base64': Use base64 (default). * - 'change' (boolean): Write entry changes to the LDIF file instead of * the entries itself. I.e. write LDAP operations * acting on the entries to the file instead of the * entries contents. This writes the changes usually * carried out by an update() to the LDIF * file. Defaults to false. * - 'lowercase' (boolean): Convert attribute names to lowercase when * writing. Defaults to false. * - 'sort' (boolean): Sort attribute names when writing entries according * to the rule: objectclass first then all other * attributes alphabetically sorted by attribute * name. Defaults to false. * - 'version' (integer): Set the LDIF version to write to the resulting * LDIF file. According to RFC 2849 currently the * only legal value for this option is 1. When this * option is set Horde_Ldap_Ldif tries to adhere * more strictly to the LDIF specification in * RFC2489 in a few places. The default is null * meaning no version information is written to the * LDIF file. * - 'wrap' (integer): Number of columns where output line wrapping shall * occur. Default is 78. Setting it to 40 or lower * inhibits wrapping. * - 'raw' (string): Regular expression to denote the names of attributes * that are to be considered binary in search results if * writing entries. Example: 'raw' => * '/(?i:^jpegPhoto|;binary)/i' * * @param string|ressource $file Filename or file handle. * @param string $mode Mode to open the file, either 'r', 'w' * or 'a'. * @param array $options Options like described above. * * @throws Horde_Ldap_Exception */ public function __construct($file, $mode = 'r', $options = array()) { // Parse options. foreach ($options as $option => $value) { if (!array_key_exists($option, $this->_options)) { throw new Horde_Ldap_Exception('Option ' . $option . ' not known'); } $this->_options[$option] = Horde_String::lower($value); } // Set version. $this->version($this->_options['version']); // Setup file mode. if (!preg_match('/^[rwa]$/', $mode)) { throw new Horde_Ldap_Exception('File mode ' . $mode . ' not supported'); } $this->_mode = $mode; // Setup file handle. if (is_resource($file)) { // TODO: checks on mode possible? $this->_fh = $file; return; } switch ($mode) { case 'r': if (!file_exists($file)) { throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for reading: file not found'); } if (!is_readable($file)) { throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for reading: permission denied'); } break; case 'w': case 'a': if (file_exists($file)) { if (!is_writable($file)) { throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for writing: permission denied'); } } else { if (!@touch($file)) { throw new Horde_Ldap_Exception('Unable to create ' . $file . ' for writing: permission denied'); } } break; } $this->_fh = @fopen($file, $this->_mode); if (!$this->_fh) { throw new Horde_Ldap_Exception('Could not open file ' . $file); } $this->_fhOpened = true; } /** * Reads one entry from the file and return it as a Horde_Ldap_Entry * object. * * @return Horde_Ldap_Entry * @throws Horde_Ldap_Exception */ public function readEntry() { // Read fresh lines, set them as current lines and create the entry. $attrs = $this->nextLines(true); if (count($attrs)) { $this->_linesCur = $attrs; } return $this->currentEntry(); } /** * Returns true when the end of the file is reached. * * @return boolean */ public function eof() { return feof($this->_fh); } /** * Writes the entry or entries to the LDIF file. * * If you want to build an LDIF file containing several entries AND you * want to call writeEntry() several times, you must open the file handle * in append mode ('a'), otherwise you will always get the last entry only. * * @todo Implement operations on whole entries (adding a whole entry). * * @param Horde_Ldap_Entry|array $entries Entry or array of entries. * * @throws Horde_Ldap_Exception */ public function writeEntry($entries) { if (!is_array($entries)) { $entries = array($entries); } foreach ($entries as $entry) { $this->_entrynum++; if (!($entry instanceof Horde_Ldap_Entry)) { throw new Horde_Ldap_Exception('Entry ' . $this->_entrynum . ' is not an Horde_Ldap_Entry object'); } if ($this->_options['change']) { $this->_changeEntry($entry); } else { $this->_writeEntry($entry); } } } /** * Writes an LDIF file that describes an entry change. * * @param Horde_Ldap_Entry $entry * * @throws Horde_Ldap_Exception */ protected function _changeEntry($entry) { // Fetch change information from entry. $entry_attrs_changes = $entry->getChanges(); $num_of_changes = count($entry_attrs_changes['add']) + count($entry_attrs_changes['replace']) + count($entry_attrs_changes['delete']); $is_changed = $num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved(); // Write version if not done yet, also write DN of entry. if ($is_changed) { if (!$this->_versionWritten) { $this->writeVersion(); } $this->_writeDN($entry->currentDN()); } // Process changes. // TODO: consider DN add! if ($entry->willBeDeleted()) { $this->_writeLine('changetype: delete'); } elseif ($entry->willBeMoved()) { $this->_writeLine('changetype: modrdn'); $olddn = Horde_Ldap_Util::explodeDN($entry->currentDN(), array('casefold' => 'none')); array_shift($olddn); $oldparent = implode(',', $olddn); $newdn = Horde_Ldap_Util::explodeDN($entry->dn(), array('casefold' => 'none')); $rdn = array_shift($newdn); $parent = implode(',', $newdn); $this->_writeLine('newrdn: ' . $rdn); $this->_writeLine('deleteoldrdn: 1'); if ($parent !== $oldparent) { $this->_writeLine('newsuperior: ' . $parent); } // TODO: What if the entry has attribute changes as well? // I think we should check for that and make a dummy // entry with the changes that is written to the LDIF file. } elseif ($num_of_changes > 0) { // Write attribute change data. $this->_writeLine('changetype: modify'); foreach ($entry_attrs_changes as $changetype => $entry_attrs) { foreach ($entry_attrs as $attr_name => $attr_values) { $this->_writeLine("$changetype: $attr_name"); if ($attr_values !== null) { $this->_writeAttribute($attr_name, $attr_values, $changetype); } $this->_writeLine('-'); } } } // Finish this entry's data if we had changes. if ($is_changed) { $this->_finishEntry(); } } /** * Writes an LDIF file that describes an entry. * * @param Horde_Ldap_Entry $entry * * @throws Horde_Ldap_Exception */ protected function _writeEntry($entry) { // Fetch attributes for further processing. $entry_attrs = $entry->getValues(); // Sort and put objectclass attributes to first position. if ($this->_options['sort']) { ksort($entry_attrs); if (isset($entry_attrs['objectclass'])) { $oc = $entry_attrs['objectclass']; unset($entry_attrs['objectclass']); $entry_attrs = array_merge(array('objectclass' => $oc), $entry_attrs); } } // Write data. if (!$this->_versionWritten) { $this->writeVersion(); } $this->_writeDN($entry->dn()); foreach ($entry_attrs as $attr_name => $attr_values) { $this->_writeAttribute($attr_name, $attr_values); } $this->_finishEntry(); } /** * Writes the version to LDIF. * * If the object's version is defined, this method allows to explicitely * write the version before an entry is written. * * If not called explicitely, it gets called automatically when writing the * first entry. * * @throws Horde_Ldap_Exception */ public function writeVersion() { if (!is_null($this->version())) { $this->_writeLine('version: ' . $this->version(), 'Unable to write version'); } $this->_versionWritten = true; } /** * Returns or sets the LDIF version. * * If called with an argument it sets the LDIF version. According to RFC * 2849 currently the only legal value for the version is 1. * * @param integer $version LDIF version to set. * * @return integer The current or new version. * @throws Horde_Ldap_Exception */ public function version($version = null) { if ($version !== null) { if ($version != 1) { throw new Horde_Ldap_Exception('Illegal LDIF version set'); } $this->_options['version'] = $version; } return $this->_options['version']; } /** * Returns the file handle the Horde_Ldap_Ldif object reads from or writes * to. * * You can, for example, use this to fetch the content of the LDIF file * manually. * * @return resource * @throws Horde_Ldap_Exception */ public function handle() { if (!is_resource($this->_fh)) { throw new Horde_Ldap_Exception('Invalid file resource'); } return $this->_fh; } /** * Cleans up. * * This method signals that the LDIF object is no longer needed. You can * use this to free up some memory and close the file handle. The file * handle is only closed, if it was opened from Horde_Ldap_Ldif. * * @throws Horde_Ldap_Exception */ public function done() { // Close file handle if we opened it. if ($this->_fhOpened) { fclose($this->handle()); } // Free variables. foreach (array_keys(get_object_vars($this)) as $name) { unset($this->$name); } } /** * Returns the current Horde_Ldap_Entry object. * * @return Horde_Ldap_Entry * @throws Horde_Ldap_Exception */ public function currentEntry() { return $this->parseLines($this->currentLines()); } /** * Parse LDIF lines of one entry into an Horde_Ldap_Entry object. * * @todo what about file inclusions and urls? * "jpegphoto:< file:///usr/local/directory/photos/fiona.jpg" * * @param array $lines LDIF lines for one entry. * * @return Horde_Ldap_Entry Horde_Ldap_Entry object for those lines. * @throws Horde_Ldap_Exception */ public function parseLines($lines) { // Parse lines into an array of attributes and build the entry. $attributes = array(); $dn = false; foreach ($lines as $line) { if (!preg_match('/^(\w+)(:|::|:<)\s(.+)$/', $line, $matches)) { // Line not in "attr: value" format -> ignore. Maybe we should // rise an error here, but this should be covered by // nextLines() already. A problem arises, if users try to feed // data of several entries to this method - the resulting entry // will get wrong attributes. However, this is already // mentioned in the method documentation above. continue; } $attr = $matches[1]; $delim = $matches[2]; $data = $matches[3]; switch ($delim) { case ':': // Normal data. $attributes[$attr][] = $data; break; case '::': // Base64 data. $attributes[$attr][] = base64_decode($data); break; case ':<': // File inclusion // TODO: Is this the job of the LDAP-client or the server? throw new Horde_Ldap_Exception('File inclusions are currently not supported'); default: throw new Horde_Ldap_Exception('Parsing error: invalid syntax at parsing entry line: ' . $line); } if (Horde_String::lower($attr) == 'dn') { // DN line detected. Save possibly decoded DN. $dn = $attributes[$attr][0]; // Remove wrongly added "dn: " attribute. unset($attributes[$attr]); } } if (!$dn) { throw new Horde_Ldap_Exception('Parsing error: unable to detect DN for entry'); } return Horde_Ldap_Entry::createFresh($dn, $attributes); } /** * Returns the lines that generated the current Horde_Ldap_Entry object. * * Returns an empty array if no lines have been read so far. * * @return array Array of lines. */ public function currentLines() { return $this->_linesCur; } /** * Returns the lines that will generate the next Horde_Ldap_Entry object. * * If you set $force to true you can iterate over the lines that build up * entries manually. Otherwise, iterating is done using {@link * readEntry()}. $force will move the file pointer forward, thus returning * the next entry lines. * * Wrapped lines will be unwrapped. Comments are stripped. * * @param boolean $force Set this to true if you want to iterate over the * lines manually * * @return array * @throws Horde_Ldap_Exception */ public function nextLines($force = false) { // If we already have those lines, just return them, otherwise read. if (count($this->_linesNext) == 0 || $force) { // Empty in case something was left (if used $force). $this->_linesNext = array(); $entry_done = false; $fh = $this->handle(); // Are we in an comment? For wrapping purposes. $commentmode = false; // How many lines with data we have read? $datalines_read = 0; while (!$entry_done && !$this->eof()) { $this->_inputLine++; // Read line. Remove line endings, we want only data; this is // okay since ending spaces should be encoded. $data = rtrim(fgets($fh)); if ($data === false) { // Error only, if EOF not reached after fgets() call. if (!$this->eof()) { throw new Horde_Ldap_Exception('Error reading from file at input line ' . $this->_inputLine); } break; } if (count($this->_linesNext) > 0 && preg_match('/^$/', $data)) { // Entry is finished if we have an empty line after we had // data. $entry_done = true; // Look ahead if the next EOF is nearby. Comments and empty // lines at the file end may cause problems otherwise. $current_pos = ftell($fh); $data = fgets($fh); while (!feof($fh)) { if (preg_match('/^\s*$/', $data) || preg_match('/^#/', $data)) { // Only empty lines or comments, continue to seek. // TODO: Known bug: Wrappings for comments are okay // but are treaten as error, since we do not // honor comment mode here. This should be a // very theoretically case, however I am // willing to fix this if really necessary. $this->_inputLine++; $current_pos = ftell($fh); $data = fgets($fh); } else { // Data found if non emtpy line and not a comment!! // Rewind to position prior last read and stop // lookahead. fseek($fh, $current_pos); break; } } // Now we have either the file pointer at the beginning of // a new data position or at the end of file causing feof() // to return true. continue; } // Build lines. if (preg_match('/^version:\s(.+)$/', $data, $match)) { // Version statement, set version. $this->version($match[1]); } elseif (preg_match('/^\w+::?\s.+$/', $data)) { // Normal attribute: add line. $commentmode = false; $this->_linesNext[] = trim($data); $datalines_read++; } elseif (preg_match('/^\s(.+)$/', $data, $matches)) { // Wrapped data: unwrap if not in comment mode. if (!$commentmode) { if ($datalines_read == 0) { // First line of entry: wrapped data is illegal. throw new Horde_Ldap_Exception('Illegal wrapping at input line ' . $this->_inputLine); } $this->_linesNext[] = array_pop($this->_linesNext) . trim($matches[1]); $datalines_read++; } } elseif (preg_match('/^#/', $data)) { // LDIF comments. $commentmode = true; } elseif (preg_match('/^\s*$/', $data)) { // Empty line but we had no data for this entry, so just // ignore this line. $commentmode = false; } else { throw new Horde_Ldap_Exception('Invalid syntax at input line ' . $this->_inputLine); } } } return $this->_linesNext; } /** * Converts an attribute and value to LDIF string representation. * * It honors correct encoding of values according to RFC 2849. Line * wrapping will occur at the configured maximum but only if the value is * greater than 40 chars. * * @param string $attr_name Name of the attribute. * @param string $attr_value Value of the attribute. * * @return string LDIF string for that attribute and value. */ protected function _convertAttribute($attr_name, $attr_value) { // Handle empty attribute or process. if (!strlen($attr_value)) { return $attr_name.': '; } // If converting is needed, do it. // Either we have some special chars or a matching "raw" regex if ($this->_isBinary($attr_value) || ($this->_options['raw'] && preg_match($this->_options['raw'], $attr_name))) { $attr_name .= ':'; $attr_value = base64_encode($attr_value); } // Lowercase attribute names if requested. if ($this->_options['lowercase']) { $attr_name = Horde_String::lower($attr_name); } // Handle line wrapping. if ($this->_options['wrap'] > 40 && strlen($attr_value) > $this->_options['wrap']) { $attr_value = wordwrap($attr_value, $this->_options['wrap'], PHP_EOL . ' ', true); } return $attr_name . ': ' . $attr_value; } /** * Converts an entry's DN to LDIF string representation. * * It honors correct encoding of values according to RFC 2849. * * @todo I am not sure, if the UTF8 stuff is correctly handled right now * * @param string $dn UTF8 encoded DN. * * @return string LDIF string for that DN. */ protected function _convertDN($dn) { // If converting is needed, do it. return $this->_isBinary($dn) ? 'dn:: ' . base64_encode($dn) : 'dn: ' . $dn; } /** * Returns whether some data is considered binary and must be * base64-encoded. * * @param string $value Some data. * * @return boolean True if the data should be encoded. */ protected function _isBinary($value) { $binary = false; // ASCII-chars that are NOT safe for the start and for being inside the // value. These are the integer values of those chars. $unsafe_init = array(0, 10, 13, 32, 58, 60); $unsafe = array(0, 10, 13); // Test for illegal init char. $init_ord = ord(substr($value, 0, 1)); if ($init_ord > 127 || in_array($init_ord, $unsafe_init)) { $binary = true; } // Test for illegal content char. for ($i = 0, $len = strlen($value); $i < $len; $i++) { $char_ord = ord(substr($value, $i, 1)); if ($char_ord >= 127 || in_array($char_ord, $unsafe)) { $binary = true; } } // Test for ending space if (substr($value, -1) == ' ') { $binary = true; } return $binary; } /** * Writes an attribute to the file handle. * * @param string $attr_name Name of the attribute. * @param string|array $attr_values Single attribute value or array with * attribute values. * * @throws Horde_Ldap_Exception */ protected function _writeAttribute($attr_name, $attr_values) { // Write out attribute content. if (!is_array($attr_values)) { $attr_values = array($attr_values); } foreach ($attr_values as $attr_val) { $line = $this->_convertAttribute($attr_name, $attr_val); $this->_writeLine($line, 'Unable to write attribute ' . $attr_name . ' of entry ' . $this->_entrynum); } } /** * 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); } /** * Finishes an LDIF entry. * * @throws Horde_Ldap_Exception */ protected function _finishEntry() { $this->_writeLine('', 'Unable to close entry ' . $this->_entrynum); } /** * Writes an arbitary line to the file handle. * * @param string $line Content to write. * @param string $error If error occurs, throw this exception message. * * @throws Horde_Ldap_Exception */ protected function _writeLine($line, $error = 'Unable to write to file handle') { $line .= PHP_EOL; if (is_resource($this->handle()) && fwrite($this->handle(), $line, strlen($line)) === false) { throw new Horde_Ldap_Exception($error); } } } Horde_Ldap-2.4.0/lib/Horde/Ldap/RootDse.php0000664000175000017500000001111613054777070016413 0ustar janjan * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_RootDse implements Serializable { /** * @var object Horde_Ldap_Entry */ protected $_entry; /** * Constructor. * * Fetches a RootDSE object from an LDAP connection. * * @param Horde_Ldap $ldap Directory from which the RootDSE should be * fetched. * @param array $attrs Array of attributes to search for. * * @throws Horde_Ldap_Exception */ public function __construct(Horde_Ldap $ldap, $attrs = null) { if (is_array($attrs) && count($attrs)) { $attributes = $attrs; } else { $attributes = array('vendorName', 'vendorVersion', 'namingContexts', 'altServer', 'supportedExtension', 'supportedControl', 'supportedSASLMechanisms', 'supportedLDAPVersion', 'subschemaSubentry'); } $referral = $ldap->getOption('LDAP_OPT_REFERRALS'); $ldap->setOption('LDAP_OPT_REFERRALS', false); try { $result = $ldap->search('', '(objectClass=*)', array('attributes' => $attributes, 'scope' => 'base')); } catch (Horde_Ldap_Exception $e) { $ldap->setOption('LDAP_OPT_REFERRALS', $referral); throw $e; } $ldap->setOption('LDAP_OPT_REFERRALS', $referral); $entry = $result->shiftEntry(); if (!$entry) { throw new Horde_Ldap_Exception('Could not fetch RootDSE entry'); } $this->_entry = $entry; } /** * Returns the requested attribute value. * * @see Horde_Ldap_Entry::getValue() * * @param string $attr Attribute name. * @param array $options Array of options. * * @return string|array Attribute value(s). * @throws Horde_Ldap_Exception */ public function getValue($attr, $options = '') { return $this->_entry->getValue($attr, $options); } /** * Determines if the extension is supported. * * @param array $oids Array of OIDs to check. * * @return boolean */ public function supportedExtension($oids) { return $this->_checkAttr($oids, 'supportedExtension'); } /** * Determines if the version is supported. * * @param array $versions Versions to check. * * @return boolean */ public function supportedVersion($versions) { return $this->_checkAttr($versions, 'supportedLDAPVersion'); } /** * Determines if the control is supported. * * @param array $oids Control OIDs to check. * * @return boolean */ public function supportedControl($oids) { return $this->_checkAttr($oids, 'supportedControl'); } /** * Determines if the sasl mechanism is supported. * * @param array $mechlist SASL mechanisms to check. * * @return boolean */ public function supportedSASLMechanism($mechlist) { return $this->checkAttr($mechlist, 'supportedSASLMechanisms'); } /** * Checks for existance of value in attribute. * * @param array $values Values to check. * @param string $attr Attribute name. * * @return boolean */ protected function _checkAttr($values, $attr) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $this->getValue($attr, 'all'))) { return false; } } return true; } /* Serializable methods */ /** * @since Horde_Ldap 2.4.0 */ public function serialize() { return serialize(array( $this->_entry->currentDN(), $this->_entry->getValues() )); } /** * @since Horde_Ldap 2.4.0 */ public function unserialize($serialized) { $data = unserialize($serialized); $this->_entry = Horde_Ldap_Entry::createFresh($data[0], $data[1]); } } Horde_Ldap-2.4.0/lib/Horde/Ldap/Schema.php0000664000175000017500000003777413054777070016256 0ustar janjan * @author Benedikt Hallinger * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_Schema { /** * Syntax definitions. * * Please don't forget to add binary attributes to isBinary() below to * support proper value fetching from Horde_Ldap_Entry. */ const SYNTAX_BOOLEAN = '1.3.6.1.4.1.1466.115.121.1.7'; const SYNTAX_DIRECTORY_STRING = '1.3.6.1.4.1.1466.115.121.1.15'; const SYNTAX_DISTINGUISHED_NAME = '1.3.6.1.4.1.1466.115.121.1.12'; const SYNTAX_INTEGER = '1.3.6.1.4.1.1466.115.121.1.27'; const SYNTAX_JPEG = '1.3.6.1.4.1.1466.115.121.1.28'; const SYNTAX_NUMERIC_STRING = '1.3.6.1.4.1.1466.115.121.1.36'; const SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.38'; const SYNTAX_OCTET_STRING = '1.3.6.1.4.1.1466.115.121.1.40'; /** * Map of entry types to LDAP attributes of subschema entry. * * @var array */ public $types = array( 'attribute' => 'attributeTypes', 'ditcontentrule' => 'dITContentRules', 'ditstructurerule' => 'dITStructureRules', 'matchingrule' => 'matchingRules', 'matchingruleuse' => 'matchingRuleUse', 'nameform' => 'nameForms', 'objectclass' => 'objectClasses', 'syntax' => 'ldapSyntaxes' ); /** * Array of entries belonging to this type * * @var array */ protected $_attributeTypes = array(); protected $_matchingRules = array(); protected $_matchingRuleUse = array(); protected $_ldapSyntaxes = array(); protected $_objectClasses = array(); protected $_dITContentRules = array(); protected $_dITStructureRules = array(); protected $_nameForms = array(); /** * Hash of all fetched OIDs. * * @var array */ protected $_oids = array(); /** * Whether the schema is initialized. * * @see parse(), get() * @var boolean */ protected $_initialized = false; /** * Constructor. * * Fetches the Schema from an LDAP connection. * * @param Horde_Ldap $ldap LDAP connection. * @param string $dn Subschema entry DN. * * @throws Horde_Ldap_Exception */ public function __construct(Horde_Ldap $ldap, $dn = null) { if (is_null($dn)) { // Get the subschema entry via rootDSE. $dse = $ldap->rootDSE(array('subschemaSubentry')); $base = $dse->getValue('subschemaSubentry', 'single'); $dn = $base; } // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that // incorrectly call this entry subSchemaSubentry instead of // subschemaSubentry. Note the correct case/spelling as per RFC 2251. if (is_null($dn)) { // Get the subschema entry via rootDSE. $dse = $ldap->rootDSE(array('subSchemaSubentry')); $base = $dse->getValue('subSchemaSubentry', 'single'); $dn = $base; } // Final fallback in case there is no subschemaSubentry attribute in // the root DSE (this is a bug for an LDAPv3 server so report this to // your LDAP vendor if you get this far). if (is_null($dn)) { $dn = 'cn=Subschema'; } // Fetch the subschema entry. $result = $ldap->search($dn, '(objectClass=*)', array('attributes' => array_values($this->types), 'scope' => 'base')); $entry = $result->shiftEntry(); if (!($entry instanceof Horde_Ldap_Entry)) { throw new Horde_Ldap_Exception('Could not fetch Subschema entry'); } $this->parse($entry); } /** * Returns a hash of entries for the given type. * * Types may be: objectclasses, attributes, ditcontentrules, * ditstructurerules, matchingrules, matchingruleuses, nameforms, syntaxes. * * @param string $type Type to fetch. * * @return array * @throws Horde_Ldap_Exception */ public function getAll($type) { $map = array('objectclasses' => $this->_objectClasses, 'attributes' => $this->_attributeTypes, 'ditcontentrules' => $this->_dITContentRules, 'ditstructurerules' => $this->_dITStructureRules, 'matchingrules' => $this->_matchingRules, 'matchingruleuses' => $this->_matchingRuleUse, 'nameforms' => $this->_nameForms, 'syntaxes' => $this->_ldapSyntaxes); $key = Horde_String::lower($type); if (!isset($map[$key])) { throw new Horde_Ldap_Exception("Unknown type $type"); } return $map[$key]; } /** * Returns a specific entry. * * @param string $type Type of name. * @param string $name Name or OID to fetch. * * @return mixed * @throws Horde_Ldap_Exception */ public function get($type, $name) { if (!$this->_initialized) { return null; } $type = Horde_String::lower($type); if (!isset($this->types[$type])) { throw new Horde_Ldap_Exception("No such type $type"); } $name = Horde_String::lower($name); $type_var = $this->{'_' . $this->types[$type]}; if (isset($type_var[$name])) { return $type_var[$name]; } if (isset($this->_oids[$name]) && $this->_oids[$name]['type'] == $type) { return $this->_oids[$name]; } throw new Horde_Ldap_Exception("Could not find $type $name"); } /** * Fetches attributes that MAY be present in the given objectclass. * * @param string $oc Name or OID of objectclass. * @param boolean $checksup Check all superiour objectclasses too? * * @return array Array with attributes. */ public function may($oc, $checksup = false) { try { $attributes = $this->_getAttr($oc, 'may'); } catch (Horde_Ldap_Exception $e) { $attributes = array(); } if ($checksup) { try { foreach ($this->superclass($oc) as $sup) { $attributes = array_merge($attributes, $this->may($sup, true)); } } catch (Horde_Ldap_Exception $e) { } $attributes = array_values(array_unique($attributes)); } return $attributes; } /** * Fetches attributes that MUST be present in the given objectclass. * * @param string $oc Name or OID of objectclass. * @param boolean $checksup Check all superiour objectclasses too? * * @return array Array with attributes. */ public function must($oc, $checksup = false) { try { $attributes = $this->_getAttr($oc, 'must'); } catch (Horde_Ldap_Exception $e) { $attributes = array(); } if ($checksup) { try { foreach ($this->superclass($oc) as $sup) { $attributes = array_merge($attributes, $this->must($sup, true)); } } catch (Horde_Ldap_Exception $e) { } $attributes = array_values(array_unique($attributes)); } return $attributes; } /** * Fetches the given attribute from the given objectclass. * * @param string $oc Name or OID of objectclass. * @param string $attr Name of attribute to fetch. * * @return array The attribute. * @throws Horde_Ldap_Exception */ protected function _getAttr($oc, $attr) { $oc = Horde_String::lower($oc); if (isset($this->_objectClasses[$oc]) && isset($this->_objectClasses[$oc][$attr])) { return $this->_objectClasses[$oc][$attr]; } if (isset($this->_oids[$oc]) && $this->_oids[$oc]['type'] == 'objectclass' && isset($this->_oids[$oc][$attr])) { return $this->_oids[$oc][$attr]; } throw new Horde_Ldap_Exception("Could not find $attr attributes for $oc "); } /** * Returns the name(s) of the immediate superclass(es). * * @param string $oc Name or OID of objectclass. * * @return array * @throws Horde_Ldap_Exception */ public function superclass($oc) { $o = $this->get('objectclass', $oc); return isset($o['sup']) ? $o['sup'] : array(); } /** * Parses the schema of the given subschema entry. * * @param Horde_Ldap_Entry $entry Subschema entry. */ public function parse($entry) { foreach ($this->types as $type => $attr) { // Initialize map type to entry. $type_var = '_' . $attr; $this->{$type_var} = array(); if (!$entry->exists($attr)) { continue; } // Get values for this type. $values = $entry->getValue($attr); if (!is_array($values)) { continue; } foreach ($values as $value) { // Get the schema entry. $schema_entry = $this->_parse_entry($value); // Set the type. $schema_entry['type'] = $type; // Save a ref in $_oids. $this->_oids[$schema_entry['oid']] = $schema_entry; // Save refs for all names in type map. $names = $schema_entry['aliases']; $names[] = $schema_entry['name']; foreach ($names as $name) { $this->{$type_var}[Horde_String::lower($name)] = $schema_entry; } } } $this->_initialized = true; } /** * Parses an attribute value into a schema entry. * * @param string $value Attribute value. * * @return array Schema entry array. */ protected function _parse_entry($value) { // Tokens that have no value associated. $noValue = array('single-value', 'obsolete', 'collective', 'no-user-modification', 'abstract', 'structural', 'auxiliary'); // Tokens that can have multiple values. $multiValue = array('must', 'may', 'sup'); // Get an array of tokens. $tokens = $this->_tokenize($value); // Remove surrounding brackets. if ($tokens[0] == '(') { array_shift($tokens); } if ($tokens[count($tokens) - 1] == ')') { array_pop($tokens); } // First token is the oid. $schema_entry = array('aliases' => array(), 'oid' => array_shift($tokens)); // Cycle over the tokens until none are left. while (count($tokens) > 0) { $token = Horde_String::lower(array_shift($tokens)); if (in_array($token, $noValue)) { // Single value token. $schema_entry[$token] = 1; } else { // Follow a string or a list if it is multivalued. if (($schema_entry[$token] = array_shift($tokens)) == '(') { // Create the list of values and cycles through the tokens // until the end of the list is reached ')'. $schema_entry[$token] = array(); while ($tmp = array_shift($tokens)) { if ($tmp == ')') { break; } if ($tmp != '$') { $schema_entry[$token][] = $tmp; } } } // Create an array if the value should be multivalued but was // not. if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) { $schema_entry[$token] = array($schema_entry[$token]); } } } // Get max length from syntax. if (isset($schema_entry['syntax'])) { if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) { $schema_entry['max_length'] = $matches[1]; } } // Force a name. if (empty($schema_entry['name'])) { $schema_entry['name'] = $schema_entry['oid']; } // Make one name the default and put the other ones into aliases. if (is_array($schema_entry['name'])) { $aliases = $schema_entry['name']; $schema_entry['name'] = array_shift($aliases); $schema_entry['aliases'] = $aliases; } return $schema_entry; } /** * Tokenizes the given value into an array of tokens. * * @param string $value String to parse. * * @return array Array of tokens. */ protected function _tokenize($value) { /* Match one big pattern where only one of the three subpatterns * matches. We are interested in the subpatterns that matched. If it * matched its value will be non-empty and so it is a token. Tokens may * be round brackets, a string, or a string enclosed by ''. */ preg_match_all("/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x", $value, $matches); $tokens = array(); // Number of tokens (full pattern match). for ($i = 0, $c = count($matches[0]); $i < $c; $i++) { // Each subpattern. for ($j = 1; $j < 4; $j++) { // Pattern match in this subpattern. if (null != trim($matches[$j][$i])) { // This is the token. $tokens[$i] = trim($matches[$j][$i]); } } } return $tokens; } /** * Returns wether a attribute syntax is binary or not. * * This method is used by Horde_Ldap_Entry to decide which PHP function * needs to be used to fetch the value in the proper format (e.g. binary or * string). * * @param string $attribute The name of the attribute (eg.: 'sn'). * * @return boolean True if the attribute is a binary type. */ public function isBinary($attribute) { // All syntax that should be treaten as containing binary values. $syntax_binary = array(self::SYNTAX_OCTET_STRING, self::SYNTAX_JPEG); // Check Syntax. try { $attr_s = $this->get('attribute', $attribute); } catch (Horde_Ldap_Exception $e) { // Attribute not found in schema, consider attr not binary. return false; } if (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) { // Syntax is defined as binary in schema return true; } // Syntax not defined as binary, or not found if attribute is a // subtype, check superior attribute syntaxes. if (isset($attr_s['sup'])) { foreach ($attr_s['sup'] as $superattr) { if ($this->isBinary($superattr)) { // Stop checking parents since we are binary. return true; } } } return false; } } Horde_Ldap-2.4.0/lib/Horde/Ldap/Search.php0000664000175000017500000003656413054777070016257 0ustar janjan * @author Benedikt Hallinger * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_Search implements Iterator { /** * Search result identifier. * * @var resource */ protected $_search; /** * LDAP resource link. * * @var resource */ protected $_link; /** * Horde_Ldap object. * * A reference of the Horde_Ldap object for passing to Horde_Ldap_Entry. * * @var Horde_Ldap */ protected $_ldap; /** * Result entry identifier. * * @var resource */ protected $_entry; /** * The errorcode from the search. * * Some errorcodes might be of interest that should not be considered * errors, for example: * - 4: LDAP_SIZELIMIT_EXCEEDED - indicates a huge search. Incomplete * results are returned. If you just want to check if there is * anything returned by the search at all, this could be catched. * - 32: no such object - search here returns a count of 0. * * @var integer */ protected $_errorCode = 0; /** * Cache for all entries already fetched from iterator interface. * * @var array */ protected $_iteratorCache = array(); /** * Attributes we searched for. * * This variable gets set from the constructor and can be retrieved through * {@link searchedAttributes()}. * * @var array */ protected $_searchedAttrs = array(); /** * Cache variable for storing entries fetched internally. * * This currently is only used by {@link pop_entry()}. * * @var array */ protected $_entry_cache = false; /** * Constructor. * * @param resource $search Search result identifier. * @param Horde_Ldap|resource $ldap Horde_Ldap object or a LDAP link * resource * @param array $attributes The searched attribute names, * see {@link $_searchedAttrs}. */ public function __construct($search, $ldap, $attributes = array()) { $this->setSearch($search); if ($ldap instanceof Horde_Ldap) { $this->_ldap = $ldap; $this->setLink($this->_ldap->getLink()); } else { $this->setLink($ldap); } $this->_errorCode = @ldap_errno($this->_link); if (is_array($attributes) && !empty($attributes)) { $this->_searchedAttrs = $attributes; } } /** * Destructor. */ public function __destruct() { @ldap_free_result($this->_search); } /** * Returns all entries from the search result. * * @return array All entries. * @throws Horde_Ldap_Exception */ public function entries() { $entries = array(); while ($entry = $this->shiftEntry()) { $entries[] = $entry; } return $entries; } /** * Get the next entry from the search result. * * This will return a valid Horde_Ldap_Entry object or false, so you can * use this method to easily iterate over the entries inside a while loop. * * @return Horde_Ldap_Entry|false Reference to Horde_Ldap_Entry object or * false if no more entries exist. * @throws Horde_Ldap_Exception */ public function shiftEntry() { if (is_null($this->_entry)) { if (!$this->_entry = @ldap_first_entry($this->_link, $this->_search)) { return false; } $entry = Horde_Ldap_Entry::createConnected($this->_ldap, $this->_entry); } else { if (!$this->_entry = @ldap_next_entry($this->_link, $this->_entry)) { return false; } $entry = Horde_Ldap_Entry::createConnected($this->_ldap, $this->_entry); } return $entry; } /** * Retrieve the next entry in the search result, but starting from last * entry. * * This is the opposite to {@link shiftEntry()} and is also very useful to * be used inside a while loop. * * @return Horde_Ldap_Entry|false * @throws Horde_Ldap_Exception */ public function popEntry() { if (false === $this->_entry_cache) { // Fetch entries into cache if not done so far. $this->_entry_cache = $this->entries(); } return count($this->_entry_cache) ? array_pop($this->_entry_cache) : false; } /** * Return entries sorted as array. * * This returns a array with sorted entries and the values. Sorting is done * with PHPs {@link array_multisort()}. * * This method relies on {@link asArray()} to fetch the raw data of the * entries. * * Please note that attribute names are case sensitive! * * Usage example: * * // To sort entries first by location, then by surname, but descending: * $entries = $search->sortedAsArray(array('locality', 'sn'), SORT_DESC); * * * @todo what about server side sorting as specified in * http://www.ietf.org/rfc/rfc2891.txt? * @todo Nuke evil eval(). * * @param array $attrs Attribute names as sort criteria. * @param integer $order Ordering direction, either constant SORT_ASC or * SORT_DESC * * @return array Sorted entries. * @throws Horde_Ldap_Exception */ public function sortedAsArray(array $attrs = array('cn'), $order = SORT_ASC) { /* New code: complete "client side" sorting */ // First some parameterchecks. if ($order != SORT_ASC && $order != SORT_DESC) { throw new Horde_Ldap_Exception('Sorting failed: sorting direction not understood! (neither constant SORT_ASC nor SORT_DESC)'); } // Fetch the entries data. $entries = $this->asArray(); // Now sort each entries attribute values. // This is neccessary because later we can only sort by one value, so // we need the highest or lowest attribute now, depending on the // selected ordering for that specific attribute. foreach ($entries as $dn => $entry) { foreach ($entry as $attr_name => $attr_values) { sort($entries[$dn][$attr_name]); if ($order == SORT_DESC) { array_reverse($entries[$dn][$attr_name]); } } } // Reformat entries array for later use with // array_multisort(). $to_sort will be a numeric array similar to // ldap_get_entries(). $to_sort = array(); foreach ($entries as $dn => $entry_attr) { $row = array('dn' => $dn); foreach ($entry_attr as $attr_name => $attr_values) { $row[$attr_name] = $attr_values; } $to_sort[] = $row; } // Build columns for array_multisort(). Each requested attribute is one // row. $columns = array(); foreach ($attrs as $attr_name) { foreach ($to_sort as $key => $row) { $columns[$attr_name][$key] =& $to_sort[$key][$attr_name][0]; } } // Sort the colums with array_multisort() if there is something to sort // and if we have requested sort columns. if (!empty($to_sort) && !empty($columns)) { $sort_params = ''; foreach ($attrs as $attr_name) { $sort_params .= '$columns[\'' . $attr_name . '\'], ' . $order . ', '; } eval("array_multisort($sort_params \$to_sort);"); } return $to_sort; } /** * Returns entries sorted as objects. * * This returns a array with sorted Horde_Ldap_Entry objects. The sorting * is actually done with {@link sortedAsArray()}. * * Please note that attribute names are case sensitive! * * Also note that it is (depending on server capabilities) possible to let * the server sort your results. This happens through search controls and * is described in detail at {@link http://www.ietf.org/rfc/rfc2891.txt} * * Usage example: * * // To sort entries first by location, then by surname, but descending: * $entries = $search->sorted(array('locality', 'sn'), SORT_DESC); * * * @todo Entry object construction could be faster. Maybe we could use one * of the factories instead of fetching the entry again. * * @param array $attrs Attribute names as sort criteria. * @param integer $order Ordering direction, either constant SORT_ASC or * SORT_DESC * * @return array Sorted entries. * @throws Horde_Ldap_Exception */ public function sorted($attrs = array('cn'), $order = SORT_ASC) { $return = array(); $sorted = $this->sortedAsArray($attrs, $order); foreach ($sorted as $row) { $entry = $this->_ldap->getEntry($row['dn'], $this->searchedAttributes()); $return[] = $entry; } return $return; } /** * Returns entries as array. * * The first array level contains all found entries where the keys are the * DNs of the entries. The second level arrays contian the entries * attributes such that the keys is the lowercased name of the attribute * and the values are stored in another indexed array. Note that the * attribute values are stored in an array even if there is no or just one * value. * * The array has the following structure: * * array( * 'cn=foo,dc=example,dc=com' => array( * 'sn' => array('foo'), * 'multival' => array('val1', 'val2', 'valN')), * 'cn=bar,dc=example,dc=com' => array( * 'sn' => array('bar'), * 'multival' => array('val1', 'valN'))) * * * @return array Associative result array as described above. * @throws Horde_Ldap_Exception */ public function asArray() { $return = array(); $entries = $this->entries(); foreach ($entries as $entry) { $attrs = array(); $entry_attributes = $entry->attributes(); foreach ($entry_attributes as $attr_name) { $attr_values = $entry->getValue($attr_name, 'all'); if (!is_array($attr_values)) { $attr_values = array($attr_values); } $attrs[$attr_name] = $attr_values; } $return[$entry->dn()] = $attrs; } return $return; } /** * Sets the search objects resource link * * @param resource $search Search result identifier. */ public function setSearch($search) { $this->_search = $search; } /** * Sets the LDAP resource link. * * @param resource $link LDAP link identifier. */ public function setLink($link) { $this->_link = $link; } /** * Returns the number of entries in the search result. * * @return integer Number of found entries. */ public function count() { // This catches the situation where OL returned errno 32 = no such // object! if (!$this->_search) { return 0; } return @ldap_count_entries($this->_link, $this->_search); } /** * Returns the errorcode from the search. * * @return integer The LDAP error number. */ public function getErrorCode() { return $this->_errorCode; } /** * Returns the attribute names this search selected. * * @see $_searchedAttrs * * @return array */ protected function searchedAttributes() { return $this->_searchedAttrs; } /** * Returns wheter this search exceeded a sizelimit. * * @return boolean True if the size limit was exceeded. */ public function sizeLimitExceeded() { return $this->getErrorCode() == 4; } /* SPL Iterator interface methods. This interface allows to use * Horde_Ldap_Search objects directly inside a foreach loop. */ /** * SPL Iterator interface: Returns the current element. * * The SPL Iterator interface allows you to fetch entries inside * a foreach() loop: foreach ($search as $dn => $entry) { ... * * Of course, you may call {@link current()}, {@link key()}, {@link next()}, * {@link rewind()} and {@link valid()} yourself. * * If the search throwed an error, it returns false. False is also * returned, if the end is reached. * * In case no call to next() was made, we will issue one, thus returning * the first entry. * * @return Horde_Ldap_Entry|false * @throws Horde_Ldap_Exception */ public function current() { if (count($this->_iteratorCache) == 0) { $this->next(); reset($this->_iteratorCache); } $entry = current($this->_iteratorCache); return $entry instanceof Horde_Ldap_Entry ? $entry : false; } /** * SPL Iterator interface: Returns the identifying key (DN) of the current * entry. * * @see current() * @return string|false DN of the current entry; false in case no entry is * returned by current(). */ public function key() { $entry = $this->current(); return $entry instanceof Horde_Ldap_Entry ? $entry->dn() :false; } /** * SPL Iterator interface: Moves forward to next entry. * * After a call to {@link next()}, {@link current()} will return the next * entry in the result set. * * @see current() * @throws Horde_Ldap_Exception */ public function next() { // Fetch next entry. If we have no entries anymore, we add false (which // is returned by shiftEntry()) so current() will complain. if (count($this->_iteratorCache) - 1 <= $this->count()) { $this->_iteratorCache[] = $this->shiftEntry(); } // Move array pointer to current element. Even if we have added all // entries, this will ensure proper operation in case we rewind(). next($this->_iteratorCache); } /** * SPL Iterator interface: Checks if there is a current element after calls * to {@link rewind()} or {@link next()}. * * Used to check if we've iterated to the end of the collection. * * @see current() * @return boolean False if there's nothing more to iterate over. */ public function valid() { return $this->current() instanceof Horde_Ldap_Entry; } /** * SPL Iterator interface: Rewinds the Iterator to the first element. * * After rewinding, {@link current()} will return the first entry in the * result set. * * @see current() */ public function rewind() { reset($this->_iteratorCache); } } Horde_Ldap-2.4.0/lib/Horde/Ldap/Util.php0000664000175000017500000005576213054777070015770 0ustar janjan * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_Util { /** * Explodes the given DN into its elements * * {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a * Distinguished Name is a sequence of Relative Distinguished Names (RDNs), * which themselves are sets of Attributes. For each RDN a array is * constructed where the RDN part is stored. * * For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded * to: * * array(array('OU=Sales', 'CN=J. Smith'), * 'DC=example', * 'DC=net') * * * [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of * the BER encoding of the X.500 AttributeValue rather than some LDAP * string syntax. These values are hex-encoded and prefixed with a #. To * distinguish such BER values, explodeDN uses references to the * actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is * exploded to: * * array(array('1.3.6.1.4.1.1466.0' => "\004\002Hi"), * array('DC' => 'example', * array('DC' => 'com')) * * See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more * information on BER. * * It also performs the following operations on the given DN: * - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=", * " ", or a hexpair and strings beginning with "#". * - Removes the leading 'OID.' characters if the type is an OID instead of * a name. * - If an RDN contains multiple parts, the parts are re-ordered so that * the attribute type names are in alphabetical order. * * $options is a list of name/value pairs, valid options are: * - casefold: Controls case folding of attribute types names. * Attribute values are not affected by this option. * The default is to uppercase. Valid values are: * - lower: Lowercase attribute types names. * - upper: Uppercase attribute type names. This is the * default. * - none: Do not change attribute type names. * - reverse: If true, the RDN sequence is reversed. * - onlyvalues: If true, then only attributes values are returned ('foo' * instead of 'cn=foo') * * @todo implement BER * @todo replace preg_replace() callbacks. * * @param string $dn The DN that should be exploded. * @param array $options Options to use. * * @return array Parts of the exploded DN. */ public static function explodeDN($dn, array $options = array()) { $options = array_merge( array( 'casefold' => 'upper', 'onlyvalues' => false, 'reverse' => false, ), $options ); // Escaping of DN and stripping of "OID.". $dn = self::canonicalDN($dn, array('casefold' => $options['casefold'])); // Splitting the DN. $dn_array = preg_split('/(? $value) { $value_u = self::unescapeDNValue($value); $rdns = self::splitRDNMultivalue($value_u[0]); // TODO: nuke code duplication if (count($rdns) > 1) { // Multivalued RDN! foreach ($rdns as $subrdn_k => $subrdn_v) { // Casefolding. if ($options['casefold'] == 'upper') { $subrdn_v = preg_replace_callback('/^(\w+=)/', $callback_upper, $subrdn_v); } if ($options['casefold'] == 'lower') { $subrdn_v = preg_replace_callback('/^(\w+=)/', $callback_lower, $subrdn_v); } if ($options['onlyvalues']) { preg_match('/(.+?)(?", * ";", "#", "=" with a special meaning in RFC 2252 are preceeded by ba * backslash. Control characters with an ASCII code < 32 are represented as * \hexpair. Finally all leading and trailing spaces are converted to * sequences of \20. * * @param string|array $values DN values that should be escaped. * * @return array The escaped values. */ public static function escapeDNValue($values) { // Parameter validation. if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // Escaping of filter meta characters. $val = addcslashes($val, '\\,+"<>;#='); // ASCII < 32 escaping. $val = self::asc2hex32($val); // Convert all leading and trailing spaces to sequences of \20. if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) { $val = str_repeat('\20', strlen($matches[1])) . $matches[2] . str_repeat('\20', strlen($matches[3])); } if (null === $val) { // Apply escaped "null" if string is empty. $val = '\0'; } $values[$key] = $val; } return $values; } /** * Unescapes DN values according to RFC 2253. * * Reverts the conversion done by escapeDNValue(). * * Any escape sequence starting with a baskslash - hexpair or special * character - will be transformed back to the corresponding character. * * @param array $values DN values. * * @return array Unescaped DN values. */ public static function unescapeDNValue($values) { // Parameter validation. if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // Strip slashes from special chars. $val = str_replace( array('\\\\', '\,', '\+', '\"', '\<', '\>', '\;', '\#', '\='), array('\\', ',', '+', '"', '<', '>', ';', '#', '='), $val); // Translate hex code into ascii. $values[$key] = self::hex2asc($val); } return $values; } /** * Converts a DN into a canonical form. * * DN can either be a string or an array as returned by explodeDN(), * which is useful when constructing a DN. The DN array may have be * indexed (each array value is a OCL=VALUE pair) or associative (array key * is OCL and value is VALUE). * * It performs the following operations on the given DN: * - Removes the leading 'OID.' characters if the type is an OID instead of * a name. * - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">", * ";", "#", "="), slashes ("/"), and any other character where the ASCII * code is < 32 as \hexpair. * - Converts all leading and trailing spaces in values to be \20. * - If an RDN contains multiple parts, the parts are re-ordered so that * the attribute type names are in alphabetical order. * * $options is a list of name/value pairs, valid options are: * * - casefold: Controls case folding of attribute type names. Attribute * values are not affected by this option. The default is to * uppercase. Valid values are: * - lower: Lowercase attribute type names. * - upper: Uppercase attribute type names. * - none: Do not change attribute type names. * - reverse: If true, the RDN sequence is reversed. * - separator: Separator to use between RDNs. Defaults to comma (','). * * The empty string "" is a valid DN, so be sure not to do a "$can_dn == * false" test, because an empty string evaluates to false. Use the "===" * operator instead. * * @param array|string $dn The DN. * @param array $options Options to use. * * @return boolean|string The canonical DN or false if the DN is not valid. */ public static function canonicalDN($dn, array $options = array()) { if ($dn === '') { // Empty DN is valid. return $dn; } // Options check. $options = array_merge( array( 'casefold' => 'upper', 'reverse' => false, 'separator' => ',', ), $options ); if (!is_array($dn)) { // It is not clear to me if the perl implementation splits by the // user defined separator or if it just uses this separator to // construct the new DN. $dn = preg_split('/(? $dn_part) { if (!is_int($dn_key)) { $assoc = true; break; } } // Convert to indexed, if associative array detected. if ($assoc) { $newdn = array(); foreach ($dn as $dn_key => $dn_part) { if (is_array($dn_part)) { // We assume here that the RDN parts are also // associative. ksort($dn_part, SORT_STRING); // Copy array as-is, so we can resolve it later. $newdn[] = $dn_part; } else { $newdn[] = $dn_key . '=' . $dn_part; } } $dn =& $newdn; } } // Escaping and casefolding. foreach ($dn as $pos => $dnval) { if (is_array($dnval)) { // Subarray detected, this means most probably that we had a // multivalued DN part, which must be resolved. $dnval_new = ''; foreach ($dnval as $subkey => $subval) { // Build RDN part. if (!is_int($subkey)) { $subval = $subkey . '=' . $subval; } $subval_processed = self::canonicalDN($subval, $options); if (false === $subval_processed) { return false; } $dnval_new .= $subval_processed . '+'; } // Store RDN part, strip last plus. $dn[$pos] = substr($dnval_new, 0, -1); } else { // Try to split multivalued RDNs into array. $rdns = self::splitRDNMultivalue($dnval); if (count($rdns) > 1) { // Multivalued RDN was detected. The RDN value is expected // to be correctly split by splitRDNMultivalue(). It's time // to sort the RDN and build the DN. $rdn_string = ''; // Sort RDN keys alphabetically. sort($rdns, SORT_STRING); foreach ($rdns as $rdn) { $subval_processed = self::canonicalDN($rdn, $options); if (false === $subval_processed) { return false; } $rdn_string .= $subval_processed . '+'; } // Store RDN part, strip last plus. $dn[$pos] = substr($rdn_string, 0, -1); } else { // No multivalued RDN. Split at first unescaped "=". $dn_comp = self::splitAttributeString($rdns[0]); if (count($dn_comp) != 2) { throw new Horde_Ldap_Exception('Invalid RDN: ' . $rdns[0]); } // Trim left whitespaces because of "cn=foo, l=bar" syntax // (whitespace after comma). $ocl = ltrim($dn_comp[0]); $val = $dn_comp[1]; // Strip 'OID.', otherwise apply casefolding and escaping. if (substr(Horde_String::lower($ocl), 0, 4) == 'oid.') { $ocl = substr($ocl, 4); } else { if ($options['casefold'] == 'upper') { $ocl = Horde_String::upper($ocl); } if ($options['casefold'] == 'lower') { $ocl = Horde_String::lower($ocl); } $ocl = self::escapeDNValue(array($ocl)); $ocl = $ocl[0]; } // Escaping of DN value. // TODO: if the value is already correctly escaped, we get // double escaping. $val = self::escapeDNValue(array($val)); $val = str_replace('/', '\/', $val[0]); $dn[$pos] = $ocl . '=' . $val; } } } if ($options['reverse']) { $dn = array_reverse($dn); } return implode($options['separator'], $dn); } /** * Escapes the given values according to RFC 2254 so that they can be * safely used in LDAP filters. * * Any control characters with an ACII code < 32 as well as the characters * with special meaning in LDAP filters "*", "(", ")", and "\" (the * backslash) are converted into the representation of a backslash followed * by two hex digits representing the hexadecimal value of the character. * * @param array $values Values to escape. * * @return array Escaped values. */ public static function escapeFilterValue($values) { // Parameter validation. if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // Escaping of filter meta characters. $val = str_replace(array('\\', '*', '(', ')'), array('\5c', '\2a', '\28', '\29'), $val); // ASCII < 32 escaping. $val = self::asc2hex32($val); if (null === $val) { // Apply escaped "null" if string is empty. $val = '\0'; } $values[$key] = $val; } return $values; } /** * Unescapes the given values according to RFC 2254. * * Reverses the conversion done by {@link escapeFilterValue()}. * * Converts any sequences of a backslash followed by two hex digits into * the corresponding character. * * @param array $values Values to unescape. * * @return array Unescaped values. */ public static function unescapeFilterValue($values = array()) { // Parameter validation. if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $value) { // Translate hex code into ascii. $values[$key] = self::hex2asc($value); } return $values; } /** * Converts all ASCII chars < 32 to "\HEX". * * @param string $string String to convert. * * @return string Hexadecimal representation of $string. */ public static function asc2hex32($string) { for ($i = 0, $len = strlen($string); $i < $len; $i++) { $char = substr($string, $i, 1); if (ord($char) < 32) { $hex = dechex(ord($char)); if (strlen($hex) == 1) { $hex = '0' . $hex; } $string = str_replace($char, '\\' . $hex, $string); } } return $string; } /** * Converts all hexadecimal expressions ("\HEX") to their original ASCII * characters. * * @author beni@php.net, heavily based on work from DavidSmith@byu.net * * @param string $string String to convert. * * @return string ASCII representation of $string. */ public static function hex2asc($string) { return preg_replace_callback( '/\\\([0-9A-Fa-f]{2})/', function($hex) { return chr(hexdec($hex[1])); }, $string); } /** * Splits a multivalued RDN value into an array. * * A RDN can contain multiple values, spearated by a plus sign. This method * returns each separate ocl=value pair of the RDN part. * * If no multivalued RDN is detected, an array containing only the original * RDN part is returned. * * For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to: * array([0] => 'OU=Sales', [1] => 'CN=J. Smith') * * The method tries to be smart if it encounters unescaped "+" characters, * but may fail, so better ensure escaped "+" in attribute names and * values. * * [BUG] If you have a multivalued RDN with unescaped plus characters and * there is a unescaped plus sign at the end of an value followed by * an attribute name containing an unescaped plus, then you will get * wrong splitting: * $rdn = 'OU=Sales+C+N=J. Smith'; * returns: * array('OU=Sales+C', 'N=J. Smith'); * The "C+" is treaten as the value of the first pair instead of as * the attribute name of the second pair. To prevent this, escape * correctly. * * @param string $rdn Part of a (multivalued) escaped RDN (e.g. ou=foo or * ou=foo+cn=bar) * * @return array The components of the multivalued RDN. */ public static function splitRDNMultivalue($rdn) { $rdns = preg_split('/(? $dn_value) { // Refresh value (foreach caches!) $dn_value = $dn[$key]; // If $dn_value is not in attr=value format, we had an unescaped // separator character inside the attr name or the value. We assume // that it was the attribute value. // TODO: To solve this, we might ask the schema. The // Horde_Ldap_Util class must remain independent from the // other classes or connections though. if (!preg_match('/.+(? * @author Jan Wagner * @author Del * @author Benedikt Hallinger * @author Ben Klang * @author Chuck Hagenbuch * @author Jan Schneider * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 */ class Horde_Ldap { /** * Class configuration array. * * - hostspec: The LDAP host to connect to (may be an array of * several hosts to try). * - port: The server port. * - version: LDAP version (defaults to 3). * - tls: When set, ldap_start_tls() is run after connecting. * - binddn: The DN to bind as when searching. * - bindpw: Password to use when searching LDAP. * - basedn: LDAP base. * - options: Hash of LDAP options to set. * - filter: Default search filter. * - scope: Default search scope. * - user: Configuration parameters for {@link findUserDN()}, * must contain 'uid', and may contain 'basedn' * entries. * - timeout: Connection timeout in seconds (defaults to 5). * - auto_reconnect: If true, the class will automatically * attempt to reconnect to the LDAP server in certain * failure conditions when attempting a search, or other * LDAP operations. Defaults to false. Note that if you * set this to true, calls to search() may block * indefinitely if there is a catastrophic server failure. * - min_backoff: Minimum reconnection delay period (in seconds). * - current_backof: Initial reconnection delay period (in seconds). * - max_backoff: Maximum reconnection delay period (in seconds). * - cache: A Horde_Cache instance for caching schema requests. * * @var array */ protected $_config = array( 'hostspec' => 'localhost', 'port' => 389, 'version' => 3, 'tls' => false, 'binddn' => '', 'bindpw' => '', 'basedn' => '', 'options' => array(), 'filter' => '(objectClass=*)', 'scope' => 'sub', 'user' => array(), 'timeout' => 5, 'auto_reconnect' => false, 'min_backoff' => 1, 'current_backoff' => 1, 'max_backoff' => 32, 'cache' => false, 'cache_root_dse' => false, 'cachettl' => 3600); /** * List of hosts we try to establish a connection to. * * @var array */ protected $_hostList = array(); /** * List of hosts that are known to be down. * * @var array */ protected $_downHostList = array(); /** * LDAP resource link. * * @var resource */ protected $_link; /** * Schema object. * * @see schema() * @var Horde_Ldap_Schema */ protected $_schema; /** * Cache for attribute encoding checks. * * @var array Hash with attribute names as key and boolean value * to determine whether they should be utf8 encoded or not. */ protected $_schemaAttrs = array(); /** * Cache for rootDSE objects. * * Hash with requested rootDSE attr names as key and rootDSE * object as value. * * Since the RootDSE object itself may request a rootDSE object, * {@link rootDSE()} caches successful requests. * Internally, Horde_Ldap needs several lookups to this object, so * caching increases performance significally. * * @var array */ protected $_rootDSE = array(); /** * Constructor. * * @see $_config * * @param array $config Configuration array. */ public function __construct($config = array()) { if (!Horde_Util::loadExtension('ldap')) { throw new Horde_Ldap_Exception('No PHP LDAP extension'); } $this->setConfig($config); $this->bind(); } /** * Destructor. */ public function __destruct() { $this->disconnect(); } /** * Sets the internal configuration array. * * @param array $config Configuration hash. */ protected function setConfig($config) { /* Parameter check -- probably should raise an error here if * config is not an array. */ if (!is_array($config)) { return; } foreach ($config as $k => $v) { if (isset($this->_config[$k])) { $this->_config[$k] = $v; } } /* Ensure the host list is an array. */ if (is_array($this->_config['hostspec'])) { $this->_hostList = $this->_config['hostspec']; } else { if (strlen($this->_config['hostspec'])) { $this->_hostList = array($this->_config['hostspec']); } else { $this->_hostList = array(); /* This will cause an error in _connect(), so * the user is notified about the failure. */ } } /* Reset the down host list, which seems like a sensible thing * to do if the config is being reset for some reason. */ $this->_downHostList = array(); } /** * Bind or rebind to the LDAP server. * * This function binds with the given DN and password to the * server. In case no connection has been made yet, it will be * started and STARTTLS issued if appropiate. * * The internal bind configuration is not being updated, so if you * call bind() without parameters, you can rebind with the * credentials provided at first connecting to the server. * * @param string $dn DN for binding. * @param string $password Password for binding. * * @throws Horde_Ldap_Exception */ public function bind($dn = null, $password = null) { /* Fetch current bind credentials. */ if (is_null($dn)) { $dn = $this->_config['binddn']; } if (is_null($password)) { $password = $this->_config['bindpw']; } /* Connect first, if we haven't so far. This will also bind * us to the server. */ if (!$this->_link) { /* Store old credentials so we can revert them later, then * overwrite config with new bind credentials. */ $olddn = $this->_config['binddn']; $oldpw = $this->_config['bindpw']; /* Overwrite bind credentials in config so * _connect() knows about them. */ $this->_config['binddn'] = $dn; $this->_config['bindpw'] = $password; /* Try to connect with provided credentials. */ $msg = $this->_connect(); /* Reset to previous config. */ $this->_config['binddn'] = $olddn; $this->_config['bindpw'] = $oldpw; return; } /* Do the requested bind as we are asked to bind manually. */ if (empty($dn)) { /* Anonymous bind. */ $msg = @ldap_bind($this->_link); } else { /* Privileged bind. */ $msg = @ldap_bind($this->_link, $dn, $password); } if (!$msg) { throw new Horde_Ldap_Exception('Bind failed: ' . @ldap_error($this->_link), @ldap_errno($this->_link)); } } /** * Connects to the LDAP server. * * This function connects to the LDAP server specified in the * configuration, binds and set up the LDAP protocol as needed. * * @throws Horde_Ldap_Exception */ protected function _connect() { /* Connecting is briefly described in RFC1777. Basicly it works like * this: * 1. set up TCP connection * 2. secure that connection if neccessary * 3a. setVersion to tell server which version we want to speak * 3b. perform bind * 3c. setVersion to tell server which version we want to speak * together with a test for supported versions * 4. set additional protocol options */ /* Return if we are already connected. */ if ($this->_link) { return; } /* Connnect to the LDAP server if we are not connected. Note that * ldap_connect() may return a link value even if no connection is * made. We need to do at least one anonymous bind to ensure that a * connection is actually valid. * * See: http://www.php.net/manual/en/function.ldap-connect.php */ /* Default error message in case all connection attempts fail but no * message is set. */ $current_error = new Horde_Ldap_Exception('Unknown connection error'); /* Catch empty $_hostList arrays. */ if (!is_array($this->_hostList) || !count($this->_hostList)) { throw new Horde_Ldap_Exception('No servers configured'); } /* Cycle through the host list. */ foreach ($this->_hostList as $host) { /* Ensure we have a valid string for host name. */ if (is_array($host)) { $current_error = new Horde_Ldap_Exception('No Servers configured'); continue; } /* Skip this host if it is known to be down. */ if (in_array($host, $this->_downHostList)) { continue; } /* Record the host that we are actually connecting to in case we * need it later. */ $this->_config['hostspec'] = $host; /* The ldap extension doesn't allow to provide connection timeouts * and seems to default to 2 minutes. Open a socket manually * instead to ping the server. */ $failed = true; $url = @parse_url($host); $sockhost = !empty($url['host']) ? $url['host'] : $host; if ($fp = @fsockopen($sockhost, $this->_config['port'], $errno, $errstr, $this->_config['timeout'])) { $failed = false; fclose($fp); } /* Attempt a connection. */ if (!$failed) { $this->_link = @ldap_connect($host, $this->_config['port']); } if (!$this->_link) { $current_error = new Horde_Ldap_Exception('Could not connect to ' . $host . ':' . $this->_config['port']); $this->_downHostList[] = $host; continue; } /* If we're supposed to use TLS, do so before we try to bind, as * some strict servers only allow binding via secure * connections. */ if ($this->_config['tls']) { try { $this->startTLS(); } catch (Horde_Ldap_Exception $e) { $current_error = $e; $this->_link = false; $this->_downHostList[] = $host; continue; } } /* Try to set the configured LDAP version on the connection if LDAP * server needs that before binding (eg OpenLDAP). * This could be necessary since RFC 1777 states that the protocol * version has to be set at the bind request. * We use force here which means that the test in the rootDSE is * skipped; this is neccessary, because some strict LDAP servers * only allow to read the LDAP rootDSE (which tells us the * supported protocol versions) with authenticated clients. * This may fail in which case we try again after binding. * In this case, most probably the bind() or setVersion() call * below will also fail, providing error messages. */ $version_set = false; $this->setVersion(0, true); /* Attempt to bind to the server. If we have credentials * configured, we try to use them, otherwise it's an anonymous * bind. * As stated by RFC 1777, the bind request should be the first * operation to be performed after the connection is established. * This may give an protocol error if the server does not support * v2 binds and the above call to setVersion() failed. * If the above call failed, we try an v2 bind here and set the * version afterwards (with checking to the rootDSE). */ try { $this->bind(); } catch (Exception $e) { /* The bind failed, discard link and save error msg. * Then record the host as down and try next one. */ if ($this->errorName($e->getCode()) == 'LDAP_PROTOCOL_ERROR' && !$version_set) { /* Provide a finer grained error message if protocol error * arises because of invalid version. */ $e = new Horde_Ldap_Exception($e->getMessage() . ' (could not set LDAP protocol version to ' . $this->_config['version'].')', $e->getCode()); } $this->_link = false; $current_error = $e; $this->_downHostList[] = $host; continue; } /* Set desired LDAP version if not successfully set before. * Here, a check against the rootDSE is performed, so we get a * error message if the server does not support the version. * The rootDSE entry should tell us which LDAP versions are * supported. However, some strict LDAP servers only allow * bound users to read the rootDSE. */ if (!$version_set) { try { $this->setVersion(); } catch (Exception $e) { $current_error = $e; $this->_link = false; $this->_downHostList[] = $host; continue; } } /* Set LDAP parameters, now that we know we have a valid * connection. */ if (isset($this->_config['options']) && is_array($this->_config['options']) && count($this->_config['options'])) { foreach ($this->_config['options'] as $opt => $val) { try { $this->setOption($opt, $val); } catch (Exception $e) { $current_error = $e; $this->_link = false; $this->_downHostList[] = $host; continue 2; } } } /* At this stage we have connected, bound, and set up options, so * we have a known good LDAP server. Time to go home. */ return; } /* All connection attempts have failed, return the last error. */ throw $current_error; } /** * Reconnects to the LDAP server. * * In case the connection to the LDAP service has dropped out for some * reason, this function will reconnect, and re-bind if a bind has been * attempted in the past. It is probably most useful when the server list * provided to the new() or _connect() function is an array rather than a * single host name, because in that case it will be able to connect to a * failover or secondary server in case the primary server goes down. * * This method just tries to re-establish the current connection. It will * sleep for the current backoff period (seconds) before attempting the * connect, and if the connection fails it will double the backoff period, * but not try again. If you want to ensure a reconnection during a * transient period of server downtime then you need to call this function * in a loop. * * @throws Horde_Ldap_Exception */ protected function _reconnect() { /* Return if we are already connected. */ if ($this->_link) { return; } /* Sleep for a backoff period in seconds. */ sleep($this->_config['current_backoff']); /* Retry all available connections. */ $this->_downHostList = array(); try { $this->_connect(); } catch (Horde_Ldap_Exception $e) { $this->_config['current_backoff'] *= 2; if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { $this->_config['current_backoff'] = $this->_config['max_backoff']; } throw $e; } /* Now we should be able to safely (re-)bind. */ try { $this->bind(); } catch (Exception $e) { $this->_config['current_backoff'] *= 2; if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { $this->_config['current_backoff'] = $this->_config['max_backoff']; } /* $this->_config['hostspec'] should have had the last connected * host stored in it by _connect(). Since we are unable to * bind to that host we can safely assume that it is down or has * some other problem. */ $this->_downHostList[] = $this->_config['hostspec']; throw $e; } /* At this stage we have connected, bound, and set up options, so we * have a known good LDAP server. Time to go home. */ $this->_config['current_backoff'] = $this->_config['min_backoff']; } /** * Closes the LDAP connection. */ public function disconnect() { @ldap_close($this->_link); } /** * Starts an encrypted session. * * @throws Horde_Ldap_Exception */ public function startTLS() { /* First try STARTTLS blindly, some servers don't even allow to receive * the rootDSE without TLS. */ if (@ldap_start_tls($this->_link)) { return; } /* Keep original error. */ $error = 'TLS not started: ' . @ldap_error($this->_link); $errno = @ldap_errno($this->_link); /* Test to see if the server supports TLS at all. * This is done via testing the extensions offered by the server. * The OID 1.3.6.1.4.1.1466.20037 tells whether TLS is supported. */ try { $rootDSE = $this->rootDSE(); } catch (Exception $e) { throw new Horde_Ldap_Exception('Unable to start TLS and unable to fetch rootDSE entry to see if TLS is supported: ' . $e->getMessage(), $e->getCode()); } try { $supported_extensions = $rootDSE->getValue('supportedExtension'); } catch (Exception $e) { throw new Horde_Ldap_Exception('Unable to start TLS and unable to fetch rootDSE attribute "supportedExtension" to see if TLS is supoported: ' . $e->getMessage(), $e->getCode()); } if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) { throw new Horde_Ldap_Exception('Server reports that it does not support TLS'); } throw new Horde_Ldap_Exception($error, $errno); } /** * Adds a new entry to the directory. * * This also links the entry to the connection used for the add, if it was * a fresh entry. * * @see HordeLdap_Entry::createFresh() * * @param Horde_Ldap_Entry $entry An LDAP entry. * * @throws Horde_Ldap_Exception */ public function add(Horde_Ldap_Entry $entry) { /* Continue attempting the add operation in a loop until we get a * success, a definitive failure, or the world ends. */ while (true) { $link = $this->getLink(); if ($link === false) { /* We do not have a successful connection yet. The call to * getLink() would have kept trying if we wanted one. */ throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ' no valid LDAP connection could be found.'); } if (@ldap_add($link, $entry->dn(), $entry->getValues())) { /* Entry successfully added, we should update its Horde_Ldap * reference in case it is not set so far (fresh entry). */ try { $entry->getLDAP(); } catch (Horde_Ldap_Exception $e) { $entry->setLDAP($this); } /* Store that the entry is present inside the directory. */ $entry->markAsNew(false); return; } /* We have a failure. What kind? We may be able to reconnect and * try again. */ $error_code = @ldap_errno($link); if ($this->errorName($error_code) != 'LDAP_OPERATIONS_ERROR' | !$this->_config['auto_reconnect']) { /* Errors other than the above are just passed back to the user * so he may react upon them. */ throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ': ' . ldap_err2str($error_code), $error_code); } /* The server has disconnected before trying the operation. We * should try again, possibly with a different server. */ $this->_link = false; $this->_reconnect(); } } /** * Deletes an entry from the directory. * * @param string|Horde_Ldap_Entry $dn DN string or Horde_Ldap_Entry. * @param boolean $recursive Should we delete all children * recursivelx as well? * @throws Horde_Ldap_Exception */ public function delete($dn, $recursive = false) { if ($dn instanceof Horde_Ldap_Entry) { $dn = $dn->dn(); } if (!is_string($dn)) { throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!'); } /* Recursive delete searches for children and calls delete for them. */ if ($recursive) { $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0); if ($result && @ldap_count_entries($this->_link, $result)) { for ($subentry = @ldap_first_entry($this->_link, $result); $subentry; $subentry = @ldap_next_entry($this->_link, $subentry)) { $this->delete(@ldap_get_dn($this->_link, $subentry), true); } } } /* Continue the delete operation in a loop until we get a success, or a * definitive failure. */ while (true) { $link = $this->getLink(); if (!$link) { /* We do not have a successful connection yet. The call to * getLink() would have kept trying if we wanted one. */ throw new Horde_Ldap_Exception('Could not add entry ' . $dn . ' no valid LDAP connection could be found.'); } $s = @ldap_delete($link, $dn); if ($s) { /* Entry successfully deleted. */ return; } /* We have a failure. What kind? We may be able to reconnect and * try again. */ $error_code = @ldap_errno($link); if ($this->errorName($error_code) == 'LDAP_OPERATIONS_ERROR' && $this->_config['auto_reconnect']) { /* The server has disconnected before trying the operation. We * should try again, possibly with a different server. */ $this->_link = false; $this->_reconnect(); } elseif ($this->errorName($error_code) == 'LDAP_NOT_ALLOWED_ON_NONLEAF') { /* Subentries present, server refused to delete. * Deleting subentries is the clients responsibility, but since * the user may not know of the subentries, we do not force * that here but instead notify the developer so he may take * actions himself. */ throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ' because of subentries. Use the recursive parameter to delete them.', $error_code); } else { /* Errors other than the above catched are just passed back to * the user so he may react upon them. */ throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ': ' . ldap_err2str($error_code), $error_code); } } } /** * Modifies an LDAP entry on the server. * * The $params argument is an array of actions and should be something like * this: * * array('add' => array('attribute1' => array('val1', 'val2'), * 'attribute2' => array('val1')), * 'delete' => array('attribute1'), * 'replace' => array('attribute1' => array('val1')), * 'changes' => array('add' => ..., * 'replace' => ..., * 'delete' => array('attribute1', 'attribute2' => array('val1'))) * * * The order of execution is as following: * 1. adds from 'add' array * 2. deletes from 'delete' array * 3. replaces from 'replace' array * 4. changes (add, replace, delete) in order of appearance * * The function calls the corresponding functions of an Horde_Ldap_Entry * object. A detailed description of array structures can be found there. * * Unlike the modification methods provided by the Horde_Ldap_Entry object, * this method will instantly carry out an update() after each operation, * thus modifying "directly" on the server. * * @see Horde_Ldap_Entry::add() * @see Horde_Ldap_Entry::delete() * @see Horde_Ldap_Entry::replace() * * @param string|Horde_Ldap_Entry $entry DN string or Horde_Ldap_Entry. * @param array $parms Array of changes * * @throws Horde_Ldap_Exception */ public function modify($entry, $parms = array()) { if (is_string($entry)) { $entry = $this->getEntry($entry); } if (!($entry instanceof Horde_Ldap_Entry)) { throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!'); } if ($unknown = array_diff(array_keys($parms), array('add', 'delete', 'replace', 'changes'))) { throw new Horde_Ldap_Exception('Unknown modify action(s): ' . implode(', ', $unknown)); } /* Perform changes mentioned separately. */ foreach (array('add', 'delete', 'replace') as $action) { if (!isset($parms[$action])) { continue; } $entry->$action($parms[$action]); $entry->setLDAP($this); /* Because the ldap_*() functions are called inside * Horde_Ldap_Entry::update(), we have to trap the error codes * issued from that if we want to support reconnection. */ while (true) { try { $entry->update(); break; } catch (Exception $e) { /* We have a failure. What kind? We may be able to * reconnect and try again. */ if ($this->errorName($e->getCode()) != 'LDAP_OPERATIONS_ERROR' || !$this->_config['auto_reconnect']) { /* Errors other than the above catched are just passed * back to the user so he may react upon them. */ throw new Horde_Ldap_Exception('Could not modify entry: ' . $e->getMessage()); } /* The server has disconnected before trying the operation. * We should try again, possibly with a different * server. */ $this->_link = false; $this->_reconnect(); } } } if (!isset($parms['changes']) || !is_array($parms['changes'])) { return; } /* Perform combined changes in 'changes' array. */ foreach ($parms['changes'] as $action => $value) { $this->modify($entry, array($action => $value)); } } /** * Runs an LDAP search query. * * $base and $filter may be ommitted. The one from config will then be * used. $base is either a DN-string or an Horde_Ldap_Entry object in which * case its DN will be used. * * $params may contain: * - scope: The scope which will be used for searching, defaults to 'sub': * - base: Just one entry * - sub: The whole tree * - one: Immediately below $base * - sizelimit: Limit the number of entries returned * (default: 0 = unlimited) * - timelimit: Limit the time spent for searching (default: 0 = unlimited) * - attrsonly: If true, the search will only return the attribute names * - attributes: Array of attribute names, which the entry should contain. * It is good practice to limit this to just the ones you * need. * * You cannot override server side limitations to sizelimit and timelimit: * You can always only lower a given limit. * * @todo implement search controls (sorting etc) * * @param string|Horde_Ldap_Entry $base LDAP searchbase. * @param string|Horde_Ldap_Filter $filter LDAP search filter. * @param array $params Array of options. * * @return Horde_Ldap_Search The search result. * @throws Horde_Ldap_Exception */ public function search($base = null, $filter = null, $params = array()) { if (is_null($base)) { $base = $this->_config['basedn']; } if ($base instanceof Horde_Ldap_Entry) { /* Fetch DN of entry, making searchbase relative to the entry. */ $base = $base->dn(); } if (is_null($filter)) { $filter = $this->_config['filter']; } if ($filter instanceof Horde_Ldap_Filter) { /* Convert Horde_Ldap_Filter to string representation. */ $filter = (string)$filter; } /* Setting search parameters. */ $sizelimit = isset($params['sizelimit']) ? $params['sizelimit'] : 0; $timelimit = isset($params['timelimit']) ? $params['timelimit'] : 0; $attrsonly = isset($params['attrsonly']) ? $params['attrsonly'] : 0; $attributes = isset($params['attributes']) ? $params['attributes'] : array(); /* Ensure $attributes to be an array in case only one attribute name * was given as string. */ if (!is_array($attributes)) { $attributes = array($attributes); } /* Reorganize the $attributes array index keys sometimes there are * problems with not consecutive indexes. */ $attributes = array_values($attributes); /* Scoping makes searches faster! */ $scope = isset($params['scope']) ? $params['scope'] : $this->_config['scope']; switch ($scope) { case 'one': $search_function = 'ldap_list'; break; case 'base': $search_function = 'ldap_read'; break; default: $search_function = 'ldap_search'; } /* Continue attempting the search operation until we get a success or a * definitive failure. */ while (true) { $link = $this->getLink(); $search = @call_user_func($search_function, $link, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit); if ($errno = @ldap_errno($link)) { $err = $this->errorName($errno); if ($err == 'LDAP_NO_SUCH_OBJECT' || $err == 'LDAP_SIZELIMIT_EXCEEDED') { return new Horde_Ldap_Search($search, $this, $attributes); } if ($err == 'LDAP_FILTER_ERROR') { /* Bad search filter. */ throw new Horde_Ldap_Exception(ldap_err2str($errno) . ' ($filter)', $errno); } if ($err == 'LDAP_OPERATIONS_ERROR' && $this->_config['auto_reconnect']) { $this->_link = false; $this->_reconnect(); } else { $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope"; throw new Horde_Ldap_Exception(ldap_err2str($errno) . $msg, $errno); } } else { return new Horde_Ldap_Search($search, $this, $attributes); } } } /** * Returns the DN of a user. * * The purpose is to quickly find the full DN of a user so it can be used * to re-bind as this user. This method requires the 'user' configuration * parameter to be set. * * @param string $user The user to find. * * @return string The user's full DN. * @throws Horde_Ldap_Exception * @throws Horde_Exception_NotFound */ public function findUserDN($user) { $filter = Horde_Ldap_Filter::combine( 'and', array(Horde_Ldap_Filter::build($this->_config['user']), Horde_Ldap_Filter::create($this->_config['user']['uid'], 'equals', $user))); $search = $this->search( isset($this->_config['user']['basedn']) ? $this->_config['user']['basedn'] : null, $filter, array('attributes' => array($this->_config['user']['uid']))); if (!$search->count()) { throw new Horde_Exception_NotFound('DN for user ' . $user . ' not found'); } $entry = $search->shiftEntry(); return $entry->currentDN(); } /** * Sets an LDAP option. * * @param string $option Option to set. * @param mixed $value Value to set option to. * * @throws Horde_Ldap_Exception */ public function setOption($option, $value) { if (!$this->_link) { throw new Horde_Ldap_Exception('Could not set LDAP option: No LDAP connection'); } if (!defined($option)) { throw new Horde_Ldap_Exception('Unkown option requested'); } if (@ldap_set_option($this->_link, constant($option), $value)) { return; } $err = @ldap_errno($this->_link); if ($err) { throw new Horde_Ldap_Exception(ldap_err2str($err), $err); } throw new Horde_Ldap_Exception('Unknown error'); } /** * Returns an LDAP option value. * * @param string $option Option to get. * * @return Horde_Ldap_Error|string Horde_Ldap_Error or option value * @throws Horde_Ldap_Exception */ public function getOption($option) { if (!$this->_link) { throw new Horde_Ldap_Exception('No LDAP connection'); } if (!defined($option)) { throw new Horde_Ldap_Exception('Unkown option requested'); } if (@ldap_get_option($this->_link, constant($option), $value)) { return $value; } $err = @ldap_errno($this->_link); if ($err) { throw new Horde_Ldap_Exception(ldap_err2str($err), $err); } throw new Horde_Ldap_Exception('Unknown error'); } /** * Returns the LDAP protocol version that is used on the connection. * * A lot of LDAP functionality is defined by what protocol version * the LDAP server speaks. This might be 2 or 3. * * @return integer The protocol version. */ public function getVersion() { if ($this->_link) { $version = $this->getOption('LDAP_OPT_PROTOCOL_VERSION'); } else { $version = $this->_config['version']; } return $version; } /** * Sets the LDAP protocol version that is used on the connection. * * @todo Checking via the rootDSE takes much time - why? fetching * and instanciation is quick! * * @param integer $version LDAP version that should be used. * @param boolean $force If set to true, the check against the rootDSE * will be skipped. * * @throws Horde_Ldap_Exception */ public function setVersion($version = 0, $force = false) { if (!$version) { $version = $this->_config['version']; } /* Check to see if the server supports this version first. * * TODO: Why is this so horribly slow? $this->rootDSE() is very fast, * as well as Horde_Ldap_RootDse(). Seems like a problem at copying the * object inside PHP?? Additionally, this is not always * reproducable... */ if (!$force) { try { $rootDSE = $this->rootDSE(); $supported_versions = $rootDSE->getValue('supportedLDAPVersion'); if (is_string($supported_versions)) { $supported_versions = array($supported_versions); } $check_ok = in_array($version, $supported_versions); } catch (Horde_Ldap_Exception $e) { /* If we don't get a root DSE, this is probably a v2 server. */ $check_ok = $version < 3; } } $check_ok = true; if ($force || $check_ok) { return $this->setOption('LDAP_OPT_PROTOCOL_VERSION', $version); } throw new Horde_Ldap_Exception('LDAP Server does not support protocol version ' . $version); } /** * 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; } /** * Returns a specific entry based on the DN. * * @todo Maybe a check against the schema should be done to be * sure the attribute type exists. * * @param string $dn DN of the entry that should be fetched. * @param array $attributes Array of Attributes to select. If ommitted, all * attributes are fetched. * * @return Horde_Ldap_Entry A Horde_Ldap_Entry object. * @throws Horde_Ldap_Exception * @throws Horde_Exception_NotFound */ public function getEntry($dn, $attributes = array()) { if (!is_array($attributes)) { $attributes = array($attributes); } $result = $this->search($dn, '(objectClass=*)', array('scope' => 'base', 'attributes' => $attributes)); if (!$result->count()) { throw new Horde_Exception_NotFound(sprintf('Could not fetch entry %s: no entry found', $dn)); } $entry = $result->shiftEntry(); if (!$entry) { throw new Horde_Ldap_Exception('Could not fetch entry (error retrieving entry from search result)'); } return $entry; } /** * Renames or moves an entry. * * This method will instantly carry out an update() after the * move, so the entry is moved instantly. * * You can pass an optional Horde_Ldap object. In this case, a * cross directory move will be performed which deletes the entry * in the source (THIS) directory and adds it in the directory * $target_ldap. * * A cross directory move will switch the entry's internal LDAP * reference so updates to the entry will go to the new directory. * * If you want to do a cross directory move, you need to pass an * Horde_Ldap_Entry object, otherwise the attributes will be * empty. * * @param string|Horde_Ldap_Entry $entry An LDAP entry. * @param string $newdn The new location. * @param Horde_Ldap $target_ldap Target directory for cross * server move. * * @throws Horde_Ldap_Exception */ public function move($entry, $newdn, $target_ldap = null) { if (is_string($entry)) { if ($target_ldap && $target_ldap !== $this) { throw new Horde_Ldap_Exception('Unable to perform cross directory move: operation requires a Horde_Ldap_Entry object'); } $entry = $this->getEntry($entry); } if (!$entry instanceof Horde_Ldap_Entry) { throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object! (If DN was passed, conversion failed)'); } if ($target_ldap && !($target_ldap instanceof Horde_Ldap)) { throw new Horde_Ldap_Exception('Parameter $target_ldap is expected to be a Horde_Ldap object!'); } if (!$target_ldap || $target_ldap === $this) { /* Local move. */ $entry->dn($newdn); $entry->setLDAP($this); $entry->update(); return; } /* Cross directory move. */ if ($target_ldap->exists($newdn)) { throw new Horde_Ldap_Exception('Unable to perform cross directory move: entry does exist in target directory'); } $entry->dn($newdn); try { $target_ldap->add($entry); } catch (Exception $e) { throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in target directory'); } try { $this->delete($entry->currentDN()); } catch (Exception $e) { try { $add_error_string = ''; /* Undo add. */ $target_ldap->delete($entry); } catch (Exception $e) { $add_error_string = ' Additionally, the deletion (undo add) of $entry in target directory failed.'; } throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in source directory.' . $add_error_string); } $entry->setLDAP($target_ldap); } /** * Copies an entry to a new location. * * The entry will be immediately copied. Only attributes you have * selected will be copied. * * @param Horde_Ldap_Entry $entry An LDAP entry. * @param string $newdn New FQF-DN of the entry. * * @return Horde_Ldap_Entry The copied entry. * @throws Horde_Ldap_Exception */ public function copy($entry, $newdn) { if (!$entry instanceof Horde_Ldap_Entry) { throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object'); } $newentry = Horde_Ldap_Entry::createFresh($newdn, $entry->getValues()); $this->add($newentry); return $newentry; } /** * Returns the string for an LDAP errorcode. * * Made to be able to make better errorhandling. Function based * on DB::errorMessage(). * * Hint: The best description of the errorcodes is found here: * http://www.directory-info.com/Ldap/LDAPErrorCodes.html * * @param integer $errorcode An error code. * * @return string The description for the error. */ public static function errorName($errorcode) { $errorMessages = array( 0x00 => 'LDAP_SUCCESS', 0x01 => 'LDAP_OPERATIONS_ERROR', 0x02 => 'LDAP_PROTOCOL_ERROR', 0x03 => 'LDAP_TIMELIMIT_EXCEEDED', 0x04 => 'LDAP_SIZELIMIT_EXCEEDED', 0x05 => 'LDAP_COMPARE_FALSE', 0x06 => 'LDAP_COMPARE_TRUE', 0x07 => 'LDAP_AUTH_METHOD_NOT_SUPPORTED', 0x08 => 'LDAP_STRONG_AUTH_REQUIRED', 0x09 => 'LDAP_PARTIAL_RESULTS', 0x0a => 'LDAP_REFERRAL', 0x0b => 'LDAP_ADMINLIMIT_EXCEEDED', 0x0c => 'LDAP_UNAVAILABLE_CRITICAL_EXTENSION', 0x0d => 'LDAP_CONFIDENTIALITY_REQUIRED', 0x0e => 'LDAP_SASL_BIND_INPROGRESS', 0x10 => 'LDAP_NO_SUCH_ATTRIBUTE', 0x11 => 'LDAP_UNDEFINED_TYPE', 0x12 => 'LDAP_INAPPROPRIATE_MATCHING', 0x13 => 'LDAP_CONSTRAINT_VIOLATION', 0x14 => 'LDAP_TYPE_OR_VALUE_EXISTS', 0x15 => 'LDAP_INVALID_SYNTAX', 0x20 => 'LDAP_NO_SUCH_OBJECT', 0x21 => 'LDAP_ALIAS_PROBLEM', 0x22 => 'LDAP_INVALID_DN_SYNTAX', 0x23 => 'LDAP_IS_LEAF', 0x24 => 'LDAP_ALIAS_DEREF_PROBLEM', 0x30 => 'LDAP_INAPPROPRIATE_AUTH', 0x31 => 'LDAP_INVALID_CREDENTIALS', 0x32 => 'LDAP_INSUFFICIENT_ACCESS', 0x33 => 'LDAP_BUSY', 0x34 => 'LDAP_UNAVAILABLE', 0x35 => 'LDAP_UNWILLING_TO_PERFORM', 0x36 => 'LDAP_LOOP_DETECT', 0x3C => 'LDAP_SORT_CONTROL_MISSING', 0x3D => 'LDAP_INDEX_RANGE_ERROR', 0x40 => 'LDAP_NAMING_VIOLATION', 0x41 => 'LDAP_OBJECT_CLASS_VIOLATION', 0x42 => 'LDAP_NOT_ALLOWED_ON_NONLEAF', 0x43 => 'LDAP_NOT_ALLOWED_ON_RDN', 0x44 => 'LDAP_ALREADY_EXISTS', 0x45 => 'LDAP_NO_OBJECT_CLASS_MODS', 0x46 => 'LDAP_RESULTS_TOO_LARGE', 0x47 => 'LDAP_AFFECTS_MULTIPLE_DSAS', 0x50 => 'LDAP_OTHER', 0x51 => 'LDAP_SERVER_DOWN', 0x52 => 'LDAP_LOCAL_ERROR', 0x53 => 'LDAP_ENCODING_ERROR', 0x54 => 'LDAP_DECODING_ERROR', 0x55 => 'LDAP_TIMEOUT', 0x56 => 'LDAP_AUTH_UNKNOWN', 0x57 => 'LDAP_FILTER_ERROR', 0x58 => 'LDAP_USER_CANCELLED', 0x59 => 'LDAP_PARAM_ERROR', 0x5a => 'LDAP_NO_MEMORY', 0x5b => 'LDAP_CONNECT_ERROR', 0x5c => 'LDAP_NOT_SUPPORTED', 0x5d => 'LDAP_CONTROL_NOT_FOUND', 0x5e => 'LDAP_NO_RESULTS_RETURNED', 0x5f => 'LDAP_MORE_RESULTS_TO_RETURN', 0x60 => 'LDAP_CLIENT_LOOP', 0x61 => 'LDAP_REFERRAL_LIMIT_EXCEEDED', 1000 => 'Unknown Error'); return isset($errorMessages[$errorcode]) ? $errorMessages[$errorcode] : 'Unknown Error (' . $errorcode . ')'; } /** * Returns a rootDSE object * * This either fetches a fresh rootDSE object or returns it from * the internal cache for performance reasons, if possible. * * @param array $attrs Array of attributes to search for. * * @return Horde_Ldap_RootDse Horde_Ldap_RootDse object * @throws Horde_Ldap_Exception */ public function rootDSE(array $attrs = array()) { /* If a cache object is registered, we use that to fetch a rootDSE * object. */ $key = 'Horde_Ldap_RootDse_' . md5(serialize(array( $this->_config['hostspec'], $this->_config['port'], $attrs ))); if (empty($this->_rootDSE[$key]) && $this->_config['cache'] && $this->_config['cache_root_dse']) { $entry = $this->_config['cache']->get( $key, $this->_config['cachettl'] ); if ($entry) { $this->_rootDSE[$key] = @unserialize($entry); } } /* See if we need to fetch a fresh object, or if we already * requested this object with the same attributes. */ if (empty($this->_rootDSE[$key])) { $this->_rootDSE[$key] = new Horde_Ldap_RootDse($this, $attrs); /* If caching is active, advise the cache to store the object. */ if ($this->_config['cache'] && $this->_config['cache_root_dse']) { $this->_config['cache']->set( $key, serialize($this->_rootDSE[$key]), $this->_config['cachettl'] ); } } return $this->_rootDSE[$key]; } /** * Returns a schema object * * @param string $dn Subschema entry dn. * * @return Horde_Ldap_Schema Horde_Ldap_Schema object * @throws Horde_Ldap_Exception */ public function schema($dn = null) { /* If a schema caching object is registered, we use that to fetch a * schema object. */ $key = 'Horde_Ldap_Schema_' . md5(serialize(array($this->_config['hostspec'], $this->_config['port'], $dn))); if (!$this->_schema && $this->_config['cache']) { $schema = $this->_config['cache']->get($key, $this->_config['cachettl']); if ($schema) { $this->_schema = @unserialize($schema); } } /* Fetch schema, if not tried before and no cached version available. * If we are already fetching the schema, we will skip fetching. */ if (!$this->_schema) { /* Store a temporary error message so subsequent calls to schema() * can detect that we are fetching the schema already. Otherwise we * will get an infinite loop at Horde_Ldap_Schema. */ $this->_schema = new Horde_Ldap_Exception('Schema not initialized'); $this->_schema = new Horde_Ldap_Schema($this, $dn); /* If schema caching is active, advise the cache to store the * schema. */ if ($this->_config['cache']) { $this->_config['cache']->set($key, serialize($this->_schema), $this->_config['cachettl']); } } if ($this->_schema instanceof Horde_Ldap_Exception) { throw $this->_schema; } return $this->_schema; } /** * Checks if PHP's LDAP extension is loaded. * * If it is not loaded, it tries to load it manually using PHP's dl(). * It knows both windows-dll and *nix-so. * * @throws Horde_Ldap_Exception */ public static function checkLDAPExtension() { if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) { throw new Horde_Ldap_Exception('Unable to locate PHP LDAP extension. Please install it before using the Horde_Ldap package.'); } } /** * @todo Remove this and expect all data to be UTF-8. * * Encodes given attributes to UTF8 if needed by schema. * * This function takes attributes in an array and then checks * against the schema if they need UTF8 encoding. If that is the * case, they will be encoded. An encoded array will be returned * and can be used for adding or modifying. * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: * $attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2')); * * @param array $attributes An array of attributes. * * @return array|Horde_Ldap_Error An array of UTF8 encoded attributes or an error. */ public function utf8Encode($attributes) { return $this->utf8($attributes, 'utf8_encode'); } /** * @todo Remove this and expect all data to be UTF-8. * * Decodes the given attribute values if needed by schema * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: * $attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2')); * * @param array $attributes Array of attributes * * @access public * @see utf8Encode() * @return array|Horde_Ldap_Error Array with decoded attribute values or Error */ public function utf8Decode($attributes) { return $this->utf8($attributes, 'utf8_decode'); } /** * @todo Remove this and expect all data to be UTF-8. * * Encodes or decodes attribute values if needed * * @param array $attributes Array of attributes * @param array $function Function to apply to attribute values * * @access protected * @return array Array of attributes with function applied to values. */ protected function utf8($attributes, $function) { if (!is_array($attributes) || array_key_exists(0, $attributes)) { throw new Horde_Ldap_Exception('Parameter $attributes is expected to be an associative array'); } if (!$this->_schema) { $this->_schema = $this->schema(); } if (!$this->_link || !function_exists($function)) { return $attributes; } if (is_array($attributes) && count($attributes) > 0) { foreach ($attributes as $k => $v) { if (!isset($this->_schemaAttrs[$k])) { try { $attr = $this->_schema->get('attribute', $k); } catch (Exception $e) { continue; } if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) { $encode = true; } else { $encode = false; } $this->_schemaAttrs[$k] = $encode; } else { $encode = $this->_schemaAttrs[$k]; } if ($encode) { if (is_array($v)) { foreach ($v as $ak => $av) { $v[$ak] = call_user_func($function, $av); } } else { $v = call_user_func($function, $v); } } $attributes[$k] = $v; } } return $attributes; } /** * Returns the LDAP link resource. * * It will loop attempting to re-establish the connection if the * connection attempt fails and auto_reconnect has been turned on * (see the _config array documentation). * * @return resource LDAP link. */ public function getLink() { if ($this->_config['auto_reconnect']) { while (true) { /* Return the link handle if we are already connected. * Otherwise try to reconnect. */ if ($this->_link) { return $this->_link; } $this->_reconnect(); } } return $this->_link; } /** * Builds an LDAP search filter fragment. * * @param string $lhs The attribute to test. * @param string $op The operator. * @param string $rhs The comparison value. * @param array $params Any additional parameters for the operator. * * @return string The LDAP search fragment. */ public static function buildClause($lhs, $op, $rhs, $params = array()) { switch ($op) { case 'LIKE': if (empty($rhs)) { return '(' . $lhs . '=*)'; } if (!empty($params['begin'])) { return sprintf('(|(%s=%s*)(%s=* %s*))', $lhs, self::quote($rhs), $lhs, self::quote($rhs)); } if (!empty($params['approximate'])) { return sprintf('(%s~=%s)', $lhs, self::quote($rhs)); } return sprintf('(%s=*%s*)', $lhs, self::quote($rhs)); default: return sprintf('(%s%s%s)', $lhs, $op, self::quote($rhs)); } } /** * Escapes characters with special meaning in LDAP searches. * * @param string $clause The string to escape. * * @return string The escaped string. */ public static function quote($clause) { return str_replace(array('\\', '(', ')', '*', "\0"), array('\\5c', '\(', '\)', '\*', "\\00"), $clause); } /** * Takes an array of DN elements and properly quotes it according to RFC * 1485. * * @param array $parts An array of tuples containing the attribute * name and that attribute's value which make * up the DN. Example: * * $parts = array( * array('cn', 'John Smith'), * array('dc', 'example'), * array('dc', 'com') * ); * * Nested arrays are supported since 2.1.0, to form * multi-valued RDNs. Example: * * $parts = array( * array( * array('cn', 'John'), * array('sn', 'Smith'), * array('o', 'Acme Inc.'), * ), * array('dc', 'example'), * array('dc', 'com') * ); * * which will result in * cn=John+sn=Smith+o=Acme Inc.,dc=example,dc=com * * @return string The properly quoted string DN. */ public static function quoteDN($parts) { return implode(',', array_map('self::_quoteRDNs', $parts)); } /** * Takes a single or a list of RDN arrays with an attribute name and value * and properly quotes it according to RFC 1485. * * @param array $attribute A tuple or array of tuples containing the * attribute name and that attribute's value which * make up the RDN. * * @return string The properly quoted string RDN. */ protected static function _quoteRDNs($attribute) { if (is_array($attribute[0])) { return implode( '+', array_map('self::_quoteRDN', $attribute) ); } else { return self::_quoteRDN($attribute); } } /** * Takes an RDN array with an attribute name and value and properly quotes * it according to RFC 1485. * * @param array $attribute A tuple containing the attribute name and that * attribute's value which make up the RDN. * * @return string The properly quoted string RDN. */ protected static function _quoteRDN($attribute) { $rdn = $attribute[0] . '='; // See if we need to quote the value. if (preg_match('/^\s|\s$|\s\s|[,+="\r\n<>#;]/', $attribute[1])) { $rdn .= '"' . str_replace('"', '\\"', $attribute[1]) . '"'; } else { $rdn .= $attribute[1]; } return $rdn; } } Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/changes.ldif0000664000175000017500000000121513054777070020654 0ustar janjan# # This is a LDIF file to test writing changes of entries # # version: 1 dn: cn=test1,ou=example,dc=cno changetype: modify delete: attr1 - delete: attr2 attr2: baz - delete: attr4 - dn:: Y249dGVzdCDDtsOkw7wsb3U9ZXhhbXBsZSxkYz1jbm8= changetype: modify add: newattr newattr: foo - delete: attr3 - replace: attr1 attr1: newvaluefor1 - replace: attr2 attr2: newvalue1for2 attr2: newvalue2for2 - dn:: OmNuPWVuZHNwYWNlLGRjPWNubyA= changetype: delete dn: cn=foo,ou=example,dc=cno changetype: modrdn newrdn: cn=Bar deleteoldrdn: 1 dn: cn=footest,ou=example,dc=cno changetype: modrdn newrdn: cn=foobartest deleteoldrdn: 1 newsuperior: ou=newexample,dc=cno Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/malformed_encoding.ldif0000664000175000017500000000044713054777070023066 0ustar janjan# # This is a LDIF file to test encoding failure # # unencoded DN version: 1 dn: cn=testöäü,ou=example,dc=cno objectclass: oc1 # unencoded attr value version: 1 dn: cn=test2,ou=example,dc=cno objectclass: testöäü cn: test2 # entry ok version: 1 dn: cn=test,ou=example,dc=cno objectclass: oc1 Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/malformed_syntax.ldif0000664000175000017500000000050613054777070022622 0ustar janjan# # This is a LDIF file to test syntax error # # wrong syntax (space too less at val of objectclass) dn: cn=test1,ou=example,dc=cno objectclass:oc1 cn: test1 attr3: foo # wrong syntax (no DN given) objectclass:oc1 cn: test_invalid attr3: foo # entry ok version: 1 dn: cn=test3,ou=example,dc=cno objectclass: oc1 attr3: foo Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/malformed_wrapping.ldif0000664000175000017500000000100013054777070023111 0ustar janjan# # This is a LDIF file to test wrapping failure # # wrong wrapping (entry must fail because DN is damaged): # (note, that there must eb an empty line below this comment, otherwise # the DN line is treaten as wrapped comment) dn: cn=test1,ou=example,dc=cno objectclass: oc1 cn: test1 # wrong syntax (literal line but no wrapped content) dn: cn=test2,ou=example,dc=cno objectclass:oc1 cn: test2 some_wrong_literal_line attr3: foo # entry ok version: 1 dn: cn=test,ou=example,dc=cno objectclass: oc1 cn: test Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/sorted_w40.ldif0000664000175000017500000000143713054777070021244 0ustar janjan# # This is a LDIF file to test reading capabilitys # It was created using options: sort=1, wrap=40 # version: 1 dn: cn=test1,ou=example,dc=cno objectclass: oc1 attr1: 12345 attr2: 1234 attr2: baz attr3: foo attr3: bar attr4: brrrzztt cn: test1 dn: cn=test blabla,ou=example,dc=cno objectclass: oc2 attr1: 12345 attr2: 1234 attr2: baz attr3: foo attr3: bar attr4:: YmxhYmxhw7bDpMO8 cn: test blabla verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb5789thvngwr789cghm738 dn:: Y249dGVzdCDDtsOkw7wsb3U9ZXhhbXBsZSxkYz1jbm8= objectclass: oc3 attr1: 12345 attr2: 1234 attr2: baz attr3: foo attr3: bar attr4:: YmxhYmxhw7bDpMO8 attr5:: ZW5kc3BhY2Ug attr6:: OmJhZGluaXRjaGFy cn:: dGVzdCDDtsOkw7w= dn:: OmNuPWVuZHNwYWNlLGRjPWNubyA= cn: endspace Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/sorted_w50.ldif0000664000175000017500000000144313054777070021242 0ustar janjan# # This is a LDIF file to test reading capabilitys # It was created using options: sort=1, wrap=50 # version: 1 dn: cn=test1,ou=example,dc=cno objectclass: oc1 attr1: 12345 attr2: 1234 attr2: baz attr3: foo attr3: bar attr4: brrrzztt cn: test1 dn: cn=test blabla,ou=example,dc=cno objectclass: oc2 attr1: 12345 attr2: 1234 attr2: baz attr3: foo attr3: bar attr4:: YmxhYmxhw7bDpMO8 cn: test blabla verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8 h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb 5789thvngwr789cghm738 dn:: Y249dGVzdCDDtsOkw7wsb3U9ZXhhbXBsZSxkYz1jbm8= objectclass: oc3 attr1: 12345 attr2: 1234 attr2: baz attr3: foo attr3: bar attr4:: YmxhYmxhw7bDpMO8 attr5:: ZW5kc3BhY2Ug attr6:: OmJhZGluaXRjaGFy cn:: dGVzdCDDtsOkw7w= dn:: OmNuPWVuZHNwYWNlLGRjPWNubyA= cn: endspace Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/unsorted_w30.ldif0000664000175000017500000000143713054777070021606 0ustar janjan# # This is a LDIF file to test reading capabilitys # It was created using options: sort=0, wrap=30 # version: 1 dn: cn=test1,ou=example,dc=cno cn: test1 attr3: foo attr3: bar attr1: 12345 attr4: brrrzztt objectclass: oc1 attr2: 1234 attr2: baz dn: cn=test blabla,ou=example,dc=cno cn: test blabla attr3: foo attr3: bar attr1: 12345 attr4:: YmxhYmxhw7bDpMO8 objectclass: oc2 attr2: 1234 attr2: baz verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb5789thvngwr789cghm738 dn:: Y249dGVzdCDDtsOkw7wsb3U9ZXhhbXBsZSxkYz1jbm8= cn:: dGVzdCDDtsOkw7w= attr3: foo attr3: bar attr1: 12345 attr4:: YmxhYmxhw7bDpMO8 objectclass: oc3 attr2: 1234 attr2: baz attr5:: ZW5kc3BhY2Ug attr6:: OmJhZGluaXRjaGFy dn:: OmNuPWVuZHNwYWNlLGRjPWNubyA= cn: endspace Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/unsorted_w50.ldif0000664000175000017500000000144313054777070021605 0ustar janjan# # This is a LDIF file to test reading capabilitys # It was created using options: sort=0, wrap=50 # version: 1 dn: cn=test1,ou=example,dc=cno cn: test1 attr3: foo attr3: bar attr1: 12345 attr4: brrrzztt objectclass: oc1 attr2: 1234 attr2: baz dn: cn=test blabla,ou=example,dc=cno cn: test blabla attr3: foo attr3: bar attr1: 12345 attr4:: YmxhYmxhw7bDpMO8 objectclass: oc2 attr2: 1234 attr2: baz verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8 h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb 5789thvngwr789cghm738 dn:: Y249dGVzdCDDtsOkw7wsb3U9ZXhhbXBsZSxkYz1jbm8= cn:: dGVzdCDDtsOkw7w= attr3: foo attr3: bar attr1: 12345 attr4:: YmxhYmxhw7bDpMO8 objectclass: oc3 attr2: 1234 attr2: baz attr5:: ZW5kc3BhY2Ug attr6:: OmJhZGluaXRjaGFy dn:: OmNuPWVuZHNwYWNlLGRjPWNubyA= cn: endspace Horde_Ldap-2.4.0/test/Horde/Ldap/fixtures/unsorted_w50_WIN.ldif0000664000175000017500000000154613054777070022326 0ustar janjan# # This is a LDIF file to test reading capabilitys with WINDOWS line endings # It was created using options: sort=0, wrap=50 # version: 1 dn: cn=test1,ou=example,dc=cno cn: test1 attr3: foo attr3: bar attr1: 12345 attr4: brrrzztt objectclass: oc1 attr2: 1234 attr2: baz dn: cn=test blabla,ou=example,dc=cno cn: test blabla attr3: foo attr3: bar attr1: 12345 attr4:: YmxhYmxhw7bDpMO8 objectclass: oc2 attr2: 1234 attr2: baz verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8 h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb 5789thvngwr789cghm738 dn:: Y249dGVzdCDDtsOkw7wsb3U9ZXhhbXBsZSxkYz1jbm8= cn:: dGVzdCDDtsOkw7w= attr3: foo attr3: bar attr1: 12345 attr4:: YmxhYmxhw7bDpMO8 objectclass: oc3 attr2: 1234 attr2: baz attr5:: ZW5kc3BhY2Ug attr6:: OmJhZGluaXRjaGFy dn:: OmNuPWVuZHNwYWNlLGRjPWNubyA= cn: endspace Horde_Ldap-2.4.0/test/Horde/Ldap/AllTests.php0000664000175000017500000000013213054777070016774 0ustar janjanrun(); Horde_Ldap-2.4.0/test/Horde/Ldap/bootstrap.php0000664000175000017500000000014313054777070017260 0ustar janjan array( 'hostspec' => 'localhost', 'port' => 389, 'basedn' => 'ou=hordetest,dc=example,dc=com', 'binddn' => 'cn=admin,dc=example,dc=com', 'bindpw' => 'secret', 'cache' => new Horde_Cache(new Horde_Cache_Storage_Mock()), 'cache_root_dse' => true, ), 'capability' => array( 'anonymous' => true, 'tls' => true ), ); Horde_Ldap-2.4.0/test/Horde/Ldap/EntryTest.php0000664000175000017500000000117213054777070017207 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_EntryTest extends PHPUnit_Framework_TestCase { public function testCreateFreshSuccess() { $entry = Horde_Ldap_Entry::createFresh('cn=test', array('attr1' => 'single', 'attr2' => array('mv1', 'mv2'))); $this->assertInstanceOf('Horde_Ldap_Entry', $entry); } } Horde_Ldap-2.4.0/test/Horde/Ldap/FilterTest.php0000664000175000017500000002511413054777070017335 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_FilterTest extends PHPUnit_Framework_TestCase { /** * Test correct parsing of filter strings through parse(). */ public function testParse() { try { Horde_Ldap_Filter::parse('some_damaged_filter_str'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::parse('(invalid=filter)(because=~no-surrounding brackets)'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::parse('((invalid=filter)(because=log_op is missing))'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::parse('(invalid-because-becauseinvalidoperator)'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::parse('(&(filterpart>=ok)(part2=~ok)(filterpart3_notok---becauseinvalidoperator))'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} $parsed1 = Horde_Ldap_Filter::parse('(&(cn=foo)(ou=bar))'); $this->assertInstanceOf('Horde_Ldap_Filter', $parsed1); $this->assertEquals('(&(cn=foo)(ou=bar))', (string)$parsed1); // In an earlier version there was a problem with the splitting of the // filter parts if the next part was also an combined filter. $parsed2_str = '(&(&(objectClass=posixgroup)(objectClass=foogroup))(uniquemember=uid=eeggs,ou=people,o=foo))'; $parsed2 = Horde_Ldap_Filter::parse($parsed2_str); $this->assertInstanceOf('Horde_Ldap_Filter', $parsed2); $this->assertEquals($parsed2_str, (string)$parsed2); // In an earlier version there was a problem parsing certain // not-combined filter strings. $parsed3_str = '(!(jpegPhoto=*))'; $parsed3 = Horde_Ldap_Filter::parse($parsed3_str); $this->assertInstanceOf('Horde_Ldap_Filter', $parsed3); $this->assertEquals($parsed3_str, (string)$parsed3); $parsed3_complex_str = '(&(someAttr=someValue)(!(jpegPhoto=*)))'; $parsed3_complex = Horde_Ldap_Filter::parse($parsed3_complex_str); $this->assertInstanceOf('Horde_Ldap_Filter', $parsed3_complex); $this->assertEquals($parsed3_complex_str, (string)$parsed3_complex); } /** * This tests the basic create() method of creating filters. */ public function testCreate() { // Test values and an array containing the filter creating methods and // an regex to test the resulting filter. $testattr = 'testattr'; $testval = 'testval'; $combinations = array( 'equals' => "/\($testattr=$testval\)/", 'begins' => "/\($testattr=$testval\*\)/", 'ends' => "/\($testattr=\*$testval\)/", 'contains' => "/\($testattr=\*$testval\*\)/", 'greater' => "/\($testattr>$testval\)/", 'less' => "/\($testattr<$testval\)/", 'greaterorequal' => "/\($testattr>=$testval\)/", 'lessorequal' => "/\($testattr<=$testval\)/", 'approx' => "/\($testattr~=$testval\)/", 'any' => "/\($testattr=\*\)/" ); foreach ($combinations as $match => $regex) { // Escaping is tested in util class. $filter = Horde_Ldap_Filter::create($testattr, $match, $testval, false); $this->assertInstanceOf('Horde_Ldap_Filter', $filter); $this->assertRegExp($regex, (string)$filter, "Filter generation failed for MatchType: $match"); } // Test creating failure. try { Horde_Ldap_Filter::create($testattr, 'test_undefined_matchingrule', $testval); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} } /** * Tests if __toString() works. */ public function testToString() { $filter = Horde_Ldap_Filter::create('foo', 'equals', 'bar'); $this->assertInstanceOf('Horde_Ldap_Filter', $filter); $this->assertEquals('(foo=bar)', (string)$filter); } /** * This tests the basic combination of filters. */ public function testCombine() { // Setup. $filter0 = Horde_Ldap_Filter::create('foo', 'equals', 'bar'); $this->assertInstanceOf('Horde_Ldap_Filter', $filter0); $filter1 = Horde_Ldap_Filter::create('bar', 'equals', 'foo'); $this->assertInstanceOf('Horde_Ldap_Filter', $filter1); $filter2 = Horde_Ldap_Filter::create('you', 'equals', 'me'); $this->assertInstanceOf('Horde_Ldap_Filter', $filter2); $filter3 = Horde_Ldap_Filter::parse('(perlinterface=used)'); $this->assertInstanceOf('Horde_Ldap_Filter', $filter3); // Negation test. $filter_not1 = Horde_Ldap_Filter::combine('not', $filter0); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_not1, 'Negation failed for literal NOT'); $this->assertEquals('(!(foo=bar))', (string)$filter_not1); $filter_not2 = Horde_Ldap_Filter::combine('!', $filter0); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_not2, 'Negation failed for logical NOT'); $this->assertEquals('(!(foo=bar))', (string)$filter_not2); $filter_not3 = Horde_Ldap_Filter::combine('!', (string)$filter0); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_not3, 'Negation failed for logical NOT'); $this->assertEquals('(!' . $filter0 . ')', (string)$filter_not3); // Combination test: OR $filter_comb_or1 = Horde_Ldap_Filter::combine('or', array($filter1, $filter2)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_or1, 'Combination failed for literal OR'); $this->assertEquals('(|(bar=foo)(you=me))', (string)$filter_comb_or1); $filter_comb_or2 = Horde_Ldap_Filter::combine('|', array($filter1, $filter2)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_or2, 'combination failed for logical OR'); $this->assertEquals('(|(bar=foo)(you=me))', (string)$filter_comb_or2); // Combination test: AND $filter_comb_and1 = Horde_Ldap_Filter::combine('and', array($filter1, $filter2)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_and1, 'Combination failed for literal AND'); $this->assertEquals('(&(bar=foo)(you=me))', (string)$filter_comb_and1); $filter_comb_and2 = Horde_Ldap_Filter::combine('&', array($filter1, $filter2)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_and2, 'combination failed for logical AND'); $this->assertEquals('(&(bar=foo)(you=me))', (string)$filter_comb_and2); // Combination test: using filter created with perl interface. $filter_comb_perl1 = Horde_Ldap_Filter::combine('and', array($filter1, $filter3)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_perl1, 'Combination failed for literal AND'); $this->assertEquals('(&(bar=foo)(perlinterface=used))', (string)$filter_comb_perl1); $filter_comb_perl2 = Horde_Ldap_Filter::combine('&', array($filter1, $filter3)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_perl2, 'combination failed for logical AND'); $this->assertEquals('(&(bar=foo)(perlinterface=used))', (string)$filter_comb_perl2); // Combination test: using filter_str instead of object $filter_comb_fstr1 = Horde_Ldap_Filter::combine('and', array($filter1, '(filter_str=foo)')); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comb_fstr1, 'Combination failed for literal AND using filter_str'); $this->assertEquals('(&(bar=foo)(filter_str=foo))', (string)$filter_comb_fstr1); // Combination test: deep combination $filter_comp_deep = Horde_Ldap_Filter::combine('and',array($filter2, $filter_not1, $filter_comb_or1, $filter_comb_perl1)); $this->assertInstanceOf('Horde_Ldap_Filter', $filter_comp_deep, 'Deep combination failed!'); $this->assertEquals('(&(you=me)(!(foo=bar))(|(bar=foo)(you=me))(&(bar=foo)(perlinterface=used)))', (string)$filter_comp_deep); // Test failure in combination try { Horde_Ldap_Filter::create('foo', 'test_undefined_matchingrule', 'bar'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('not', 'damaged_filter_str'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('not', array($filter0, $filter1)); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('not', null); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('and', $filter_not1); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('and', array($filter_not1)); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('and', $filter_not1); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('or', array($filter_not1)); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('some_unknown_method', array($filter_not1)); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('and', array($filter_not1, 'some_invalid_filterstring')); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { Horde_Ldap_Filter::combine('and', array($filter_not1, null)); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} } } Horde_Ldap-2.4.0/test/Horde/Ldap/LdapTest.php0000664000175000017500000006326213054777070016776 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_LdapTest extends Horde_Ldap_TestBase { public static function tearDownAfterClass() { if (!self::$ldapcfg) { return; } $clean = array('cn=Horde_Ldap_TestEntry,', 'ou=Horde_Ldap_Test_subdelete,', 'ou=Horde_Ldap_Test_modify,', 'ou=Horde_Ldap_Test_search1,', 'ou=Horde_Ldap_Test_search2,', 'ou=Horde_Ldap_Test_exists,', 'ou=Horde_Ldap_Test_exists_2+l=somewhere,', 'ou=Horde_Ldap_Test_getEntry,', 'ou=Horde_Ldap_Test_move,', 'ou=Horde_Ldap_Test_pool,', 'ou=Horde_Ldap_Test_tgt,'); try { $ldap = new Horde_Ldap(self::$ldapcfg['server']); foreach ($clean as $dn) { try { $ldap->delete($dn . self::$ldapcfg['server']['basedn'], true); } catch (Exception $e) { } } } catch (Exception $e) { } } /** * Tests if the server can connect and bind correctly. */ public function testConnectAndPrivilegedBind() { // This connect is supposed to fail. $lcfg = array( 'hostspec' => 'nonexistant.ldap.horde.org', 'timeout' => 1, ); try { $ldap = new Horde_Ldap($lcfg); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Failing with multiple hosts. $lcfg = array( 'hostspec' => array( 'nonexistant1.ldap.horde.org', 'nonexistant2.ldap.horde.org' ), 'timeout' => 1, ); try { $ldap = new Horde_Ldap($lcfg); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Simple working connect and privileged bind. $ldap = new Horde_Ldap(self::$ldapcfg['server']); // Working connect and privileged bind with first host down. $lcfg = array( 'hostspec' => array( 'nonexistant.ldap.horde.org', self::$ldapcfg['server']['hostspec'] ), 'port' => self::$ldapcfg['server']['port'], 'binddn' => self::$ldapcfg['server']['binddn'], 'bindpw' => self::$ldapcfg['server']['bindpw'], 'timeout' => 1, ); $ldap = new Horde_Ldap($lcfg); } /** * Tests if the server can connect and bind anonymously, if supported. */ public function testConnectAndAnonymousBind() { if (!self::$ldapcfg['capability']['anonymous']) { $this->markTestSkipped('Server does not support anonymous bind'); } // Simple working connect and anonymous bind. $lcfg = array('hostspec' => self::$ldapcfg['server']['hostspec'], 'port' => self::$ldapcfg['server']['port']); $ldap = new Horde_Ldap($lcfg); } /** * Tests if the server can connect and bind, but not rebind with empty * password. * * @expectedException Horde_Ldap_Exception */ public function testConnectAndEmptyRebind() { // Simple working connect and privileged bind. $ldap = new Horde_Ldap(self::$ldapcfg['server']); $ldap->bind(self::$ldapcfg['server']['binddn'], ''); } /** * Tests startTLS() if server supports it. */ public function testStartTLS() { if (!self::$ldapcfg['capability']['tls']) { $this->markTestSkipped('Server does not support TLS'); } // Simple working connect and privileged bind. $lcfg = array('starttls' => true) + self::$ldapcfg['server']; $ldap = new Horde_Ldap($lcfg); } /** * Test if adding and deleting a fresh entry works. */ public function testAdd() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // Adding a fresh entry. $cn = 'Horde_Ldap_TestEntry'; $dn = 'cn=' . $cn . ',' . self::$ldapcfg['server']['basedn']; $fresh_entry = Horde_Ldap_Entry::createFresh( $dn, array('objectClass' => array('top', 'person'), 'cn' => $cn, 'sn' => 'TestEntry')); $this->assertInstanceOf('Horde_Ldap_Entry', $fresh_entry); $ldap->add($fresh_entry); // Deleting this entry. $ldap->delete($fresh_entry); } /** * Basic deletion is tested in testAdd(), so here we just test if * advanced deletion tasks work properly. */ public function testDelete() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // Some parameter checks. try { $ldap->delete(1234); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} try { $ldap->delete($ldap); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // In order to test subtree deletion, we need some little tree // which we need to establish first. $base = self::$ldapcfg['server']['basedn']; $testdn = 'ou=Horde_Ldap_Test_subdelete,' . $base; $ou = Horde_Ldap_Entry::createFresh( $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_subdelete')); $ou_1 = Horde_Ldap_Entry::createFresh( 'ou=test1,' . $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'test1')); $ou_1_l1 = Horde_Ldap_Entry::createFresh( 'l=subtest,ou=test1,' . $testdn, array('objectClass' => array('top', 'locality'), 'l' => 'test1')); $ou_2 = Horde_Ldap_Entry::createFresh( 'ou=test2,' . $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'test2')); $ou_3 = Horde_Ldap_Entry::createFresh( 'ou=test3,' . $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'test3')); $ldap->add($ou); $ldap->add($ou_1); $ldap->add($ou_1_l1); $ldap->add($ou_2); $ldap->add($ou_3); $this->assertTrue($ldap->exists($ou->dn())); $this->assertTrue($ldap->exists($ou_1->dn())); $this->assertTrue($ldap->exists($ou_1_l1->dn())); $this->assertTrue($ldap->exists($ou_2->dn())); $this->assertTrue($ldap->exists($ou_3->dn())); // Tree established now. We can run some tests now :D // Try to delete some non existent entry inside that subtree (fails). try { $ldap->delete('cn=not_existent,ou=test1,' . $testdn); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) { $this->assertEquals('LDAP_NO_SUCH_OBJECT', Horde_Ldap::errorName($e->getCode())); } // Try to delete main test ou without recursive set (fails too). try { $ldap->delete($testdn); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) { $this->assertEquals('LDAP_NOT_ALLOWED_ON_NONLEAF', Horde_Ldap::errorName($e->getCode())); } // Retry with subtree delete, this should work. $ldap->delete($testdn, true); // The DN is not allowed to exist anymore. $this->assertFalse($ldap->exists($testdn)); } /** * Test modify(). */ public function testModify() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // We need a test entry. $local_entry = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_modify,' . self::$ldapcfg['server']['basedn'], array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_modify', 'street' => 'Beniroad', 'telephoneNumber' => array('1234', '5678'), 'postalcode' => '12345', 'postalAddress' => 'someAddress', 'st' => array('State 1', 'State 2'))); $ldap->add($local_entry); $this->assertTrue($ldap->exists($local_entry->dn())); // Test invalid actions. try { $ldap->modify($local_entry, array('foo' => 'bar')); $this->fail('Expected exception when passing invalid actions to modify().'); } catch (Horde_Ldap_Exception $e) { } // Prepare some changes. $changes = array( 'add' => array( 'businessCategory' => array('foocat', 'barcat'), 'description' => 'testval' ), 'delete' => array('postalAddress'), 'replace' => array('telephoneNumber' => array('345', '567')), 'changes' => array( 'replace' => array('street' => 'Highway to Hell'), 'add' => array('l' => 'someLocality'), 'delete' => array( 'postalcode', 'st' => array('State 1')))); // Perform those changes. $ldap->modify($local_entry, $changes); // Verify correct attribute changes. $actual_entry = $ldap->getEntry($local_entry->dn(), array('objectClass', 'ou', 'postalAddress', 'street', 'telephoneNumber', 'postalcode', 'st', 'l', 'businessCategory', 'description')); $this->assertInstanceOf('Horde_Ldap_Entry', $actual_entry); $expected_attributes = array( 'objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_modify', 'street' => 'Highway to Hell', 'l' => 'someLocality', 'telephoneNumber' => array('345', '567'), 'businessCategory' => array('foocat', 'barcat'), 'description' => 'testval', 'st' => 'State 2' ); $local_attributes = $local_entry->getValues(); $actual_attributes = $actual_entry->getValues(); // To enable easy check, we need to sort the values of the remaining // multival attributes as well as the attribute names. ksort($expected_attributes); ksort($local_attributes); ksort($actual_attributes); sort($expected_attributes['businessCategory']); sort($local_attributes['businessCategory']); sort($actual_attributes['businessCategory']); // The attributes must match the expected values. Both, the entry // inside the directory and our local copy must reflect the same // values. $this->assertEquals($expected_attributes, $actual_attributes, 'The directory entries attributes are not OK!'); $this->assertEquals($expected_attributes, $local_attributes, 'The local entries attributes are not OK!'); } /** * Test search(). */ public function testSearch() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // Some testdata, so we can test sizelimit. $base = self::$ldapcfg['server']['basedn']; $ou1 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_search1,' . $base, array('objectClass' => array('top','organizationalUnit'), 'ou' => 'Horde_Ldap_Test_search1')); $ou1_1 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_search1_1,' . $ou1->dn(), array('objectClass' => array('top','organizationalUnit'), 'ou' => 'Horde_Ldap_Test_search1_1')); $ou2 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_search2,' . $base, array('objectClass' => array('top','organizationalUnit'), 'ou' => 'Horde_Ldap_Test_search2')); $ldap->add($ou1); $this->assertTrue($ldap->exists($ou1->dn())); $ldap->add($ou1_1); $this->assertTrue($ldap->exists($ou1_1->dn())); $ldap->add($ou2); $this->assertTrue($ldap->exists($ou2->dn())); // Search for test filter, should at least return our two test entries. $res = $ldap->search(null, '(ou=Horde_Ldap*)', array('attributes' => '1.1')); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertThat($res->count(), $this->greaterThanOrEqual(2)); // Same, but with Horde_Ldap_Filter object. $filtero = Horde_Ldap_Filter::create('ou', 'begins', 'Horde_Ldap'); $this->assertInstanceOf('Horde_Ldap_Filter', $filtero); $res = $ldap->search(null, $filtero, array('attributes' => '1.1')); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertThat($res->count(), $this->greaterThanOrEqual(2)); // Search using default filter for base-onelevel scope, should at least // return our two test entries. $res = $ldap->search(null, null, array('scope' => 'one', 'attributes' => '1.1')); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertThat($res->count(), $this->greaterThanOrEqual(2)); // Base-search using custom base (string), should only return the test // entry $ou1 and not the entry below it. $res = $ldap->search($ou1->dn(), null, array('scope' => 'base', 'attributes' => '1.1')); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertEquals(1, $res->count()); // Search using custom base, this time using an entry object. This // tests if passing an entry object as base works, should only return // the test entry $ou1. $res = $ldap->search($ou1, '(ou=*)', array('scope' => 'base', 'attributes' => '1.1')); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertEquals(1, $res->count()); // Search using default filter for base-onelevel scope with sizelimit, // should of course return more than one entry, but not more than // sizelimit $res = $ldap->search( null, null, array('scope' => 'one', 'sizelimit' => 1, 'attributes' => '1.1') ); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertEquals(1, $res->count()); // Sizelimit should be exceeded now. $this->assertTrue($res->sizeLimitExceeded()); // Bad filter. try { $res = $ldap->search(null, 'somebadfilter', array('attributes' => '1.1')); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Bad base. try { $res = $ldap->search('badbase', null, array('attributes' => '1.1')); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Nullresult. $res = $ldap->search(null, '(cn=nevermatching_filter)', array('scope' => 'base', 'attributes' => '1.1')); $this->assertInstanceOf('Horde_Ldap_Search', $res); $this->assertEquals(0, $res->count()); } /** * Test exists(). */ public function testExists() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); $dn = 'ou=Horde_Ldap_Test_exists,' . self::$ldapcfg['server']['basedn']; // Testing not existing DN. $this->assertFalse($ldap->exists($dn)); // Passing an entry object (should work). exists() should return false, // because we didn't add the test entry yet. $ou1 = Horde_Ldap_Entry::createFresh( $dn, array('objectClass' => array('top', 'organizationalUnit')) ); $this->assertFalse($ldap->exists($dn)); $this->assertFalse($ldap->exists($ou1)); // Testing not existing DN. $ldap->add($ou1); $this->assertTrue($ldap->exists($dn)); // Passing an float instead of a string. try { $ldap->exists(1.234); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Testing multivalued RDNs. $dn = 'ou=Horde_Ldap_Test_exists_2+l=somewhere,' . self::$ldapcfg['server']['basedn']; $ou2 = Horde_Ldap_Entry::createFresh( $dn, array('objectClass' => array('top', 'organizationalUnit')) ); $this->assertFalse($ldap->exists($dn)); $ldap->add($ou2); $this->assertTrue($ldap->exists($dn)); } /** * Test getEntry(). */ public function testGetEntry() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); $dn = 'ou=Horde_Ldap_Test_getEntry,' . self::$ldapcfg['server']['basedn']; $entry = Horde_Ldap_Entry::createFresh( $dn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_getEntry')); $ldap->add($entry); // Existing DN. $this->assertInstanceOf('Horde_Ldap_Entry', $ldap->getEntry($dn)); // Not existing DN. try { $ldap->getEntry('cn=notexistent,' . self::$ldapcfg['server']['basedn']); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Exception_NotFound $e) {} } /** * Test move(). */ public function testMove() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // For Moving tests, we need some little tree again. $base = self::$ldapcfg['server']['basedn']; $testdn = 'ou=Horde_Ldap_Test_move,' . $base; $ou = Horde_Ldap_Entry::createFresh( $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_move')); $ou_1 = Horde_Ldap_Entry::createFresh( 'ou=source,' . $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'source')); $ou_1_l1 = Horde_Ldap_Entry::createFresh( 'l=moveitem,ou=source,' . $testdn, array('objectClass' => array('top','locality'), 'l' => 'moveitem', 'description' => 'movetest')); $ou_2 = Horde_Ldap_Entry::createFresh( 'ou=target,' . $testdn, array('objectClass' => array('top', 'organizationalUnit'), 'ou' => 'target')); $ou_3 = Horde_Ldap_Entry::createFresh( 'ou=target_otherdir,' . $testdn, array('objectClass' => array('top','organizationalUnit'), 'ou' => 'target_otherdir')); $ldap->add($ou); $ldap->add($ou_1); $ldap->add($ou_1_l1); $ldap->add($ou_2); $ldap->add($ou_3); $this->assertTrue($ldap->exists($ou->dn())); $this->assertTrue($ldap->exists($ou_1->dn())); $this->assertTrue($ldap->exists($ou_1_l1->dn())); $this->assertTrue($ldap->exists($ou_2->dn())); $this->assertTrue($ldap->exists($ou_3->dn())); // Tree established. // Local rename. $olddn = $ou_1_l1->currentDN(); $ldap->move($ou_1_l1, str_replace('moveitem', 'move_item', $ou_1_l1->dn())); $this->assertTrue($ldap->exists($ou_1_l1->dn())); $this->assertFalse($ldap->exists($olddn)); // Local move. $olddn = $ou_1_l1->currentDN(); $ldap->move($ou_1_l1, 'l=move_item,' . $ou_2->dn()); $this->assertTrue($ldap->exists($ou_1_l1->dn())); $this->assertFalse($ldap->exists($olddn)); // Local move backward, with rename. Here we use the DN of the object, // to test DN conversion. // Note that this will outdate the object since it does not has // knowledge about the move. $olddn = $ou_1_l1->currentDN(); $newdn = 'l=moveditem,' . $ou_2->dn(); $ldap->move($olddn, $newdn); $this->assertTrue($ldap->exists($newdn)); $this->assertFalse($ldap->exists($olddn)); // Refetch since the object's DN was outdated. $ou_1_l1 = $ldap->getEntry($newdn); // Fake-cross directory move using two separate links to the same // directory. This other directory is represented by // ou=target_otherdir. $ldap2 = new Horde_Ldap(self::$ldapcfg['server']); $olddn = $ou_1_l1->currentDN(); $ldap->move($ou_1_l1, 'l=movedcrossdir,' . $ou_3->dn(), $ldap2); $this->assertFalse($ldap->exists($olddn)); $this->assertTrue($ldap2->exists($ou_1_l1->dn())); // Try to move over an existing entry. try { $ldap->move($ou_2, $ou_3->dn(), $ldap2); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Try cross directory move without providing an valid entry but a DN. try { $ldap->move($ou_1_l1->dn(), 'l=movedcrossdir2,'.$ou_2->dn(), $ldap2); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Try passing an invalid entry object. try { $ldap->move($ldap, 'l=move_item,'.$ou_2->dn()); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Try passing an invalid LDAP object. try { $ldap->move($ou_1_l1, 'l=move_item,'.$ou_2->dn(), $ou_1); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} } /** * Test copy(). */ public function testCopy() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // Some testdata. $base = self::$ldapcfg['server']['basedn']; $ou1 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_pool,' . $base, array('objectClass' => array('top','organizationalUnit'), 'ou' => 'Horde_Ldap_Test_copy')); $ou2 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_tgt,' . $base, array('objectClass' => array('top','organizationalUnit'), 'ou' => 'Horde_Ldap_Test_copy')); $ldap->add($ou1); $this->assertTrue($ldap->exists($ou1->dn())); $ldap->add($ou2); $this->assertTrue($ldap->exists($ou2->dn())); $entry = Horde_Ldap_Entry::createFresh( 'l=cptest,' . $ou1->dn(), array('objectClass' => array('top','locality'), 'l' => 'cptest')); $ldap->add($entry); $ldap->exists($entry->dn()); // Copy over the entry to another tree with rename. $entrycp = $ldap->copy($entry, 'l=test_copied,' . $ou2->dn()); $this->assertInstanceOf('Horde_Ldap_Entry', $entrycp); $this->assertNotEquals($entry->dn(), $entrycp->dn()); $this->assertTrue($ldap->exists($entrycp->dn())); // Copy same again (fails, entry exists). try { $entrycp_f = $ldap->copy($entry, 'l=test_copied,' . $ou2->dn()); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Use only DNs to copy (fails). try { $entrycp = $ldap->copy($entry->dn(), 'l=test_copied2,' . $ou2->dn()); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} } /** * Tests retrieval of root DSE object. */ public function testRootDSE() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); $this->assertInstanceOf('Horde_Ldap_RootDse', $ldap->rootDSE()); } /** * Tests retrieval of schema through LDAP object. */ public function testSchema() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); $this->assertInstanceOf('Horde_Ldap_Schema', $ldap->schema()); } /** * Test getLink(). */ public function testGetLink() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); $this->assertTrue(is_resource($ldap->getLink())); } public function testQuoteDN() { $this->assertEquals( 'cn=John Smith,dc=example,dc=com', Horde_Ldap::quoteDN( array( array('cn', 'John Smith'), array('dc', 'example'), array('dc', 'com') ) ) ); $this->assertEquals( 'cn=John+sn=Smith+o=Acme Inc.,dc=example,dc=com', Horde_Ldap::quoteDN( array( array( array('cn', 'John'), array('sn', 'Smith'), array('o', 'Acme Inc.'), ), array('dc', 'example'), array('dc', 'com') ) ) ); $this->assertEquals( 'cn=John+sn=Smith+o=Acme Inc.', Horde_Ldap::quoteDN( array( array( array('cn', 'John'), array('sn', 'Smith'), array('o', 'Acme Inc.'), ), ) ) ); } } Horde_Ldap-2.4.0/test/Horde/Ldap/LdifTest.php0000664000175000017500000004657513054777070017004 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_LdifTest extends PHPUnit_Framework_TestCase { /** * Default configuration for tests. * * The config is bound to the ldif test file * tests/fixtures/unsorted_w50.ldif, so don't change or tests will fail. * * @var array */ protected $_defaultConfig = array( 'encode' => 'base64', 'wrap' => 50, 'change' => 0, 'sort' => 0, 'version' => 1 ); /** * Test entries data. * * Please do not just modify these values, they are closely related to the * LDIF test data. * * @var array */ protected $_testdata = array( 'cn=test1,ou=example,dc=cno' => array( 'cn' => 'test1', 'attr3' => array('foo', 'bar'), 'attr1' => 12345, 'attr4' => 'brrrzztt', 'objectclass' => 'oc1', 'attr2' => array('1234', 'baz')), 'cn=test blabla,ou=example,dc=cno' => array( 'cn' => 'test blabla', 'attr3' => array('foo', 'bar'), 'attr1' => 12345, 'attr4' => 'blablaöäü', 'objectclass' => 'oc2', 'attr2' => array('1234', 'baz'), 'verylong' => 'fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb5789thvngwr789cghm738'), 'cn=test öäü,ou=example,dc=cno' => array( 'cn' => 'test öäü', 'attr3' => array('foo', 'bar'), 'attr1' => 12345, 'attr4' => 'blablaöäü', 'objectclass' => 'oc3', 'attr2' => array('1234', 'baz'), 'attr5' => 'endspace ', 'attr6' => ':badinitchar'), ':cn=endspace,dc=cno ' => array( 'cn' => 'endspace') ); /** * Test file written to. * * @var string */ protected $_outfile = 'test.out.ldif'; /** * Test entries. * * They will be created in setUp() * * @var array */ protected $_testentries; /** * Opens an outfile and ensures correct permissions. */ public function setUp() { // Initialize test entries. $this->_testentries = array(); foreach ($this->_testdata as $dn => $attrs) { $entry = Horde_Ldap_Entry::createFresh($dn, $attrs); $this->assertInstanceOf('Horde_Ldap_Entry', $entry); $this->_testentries[] = $entry; } // Create outfile if not exists and enforce proper access rights. if (!file_exists($this->_outfile)) { if (!touch($this->_outfile)) { $this->markTestSkipped('Unable to create ' . $this->_outfile); } } if (!chmod($this->_outfile, 0644)) { $this->markTestSkipped('Unable to chmod(0644) ' . $this->_outfile); } } /** * Removes the outfile. */ public function tearDown() { @unlink($this->_outfile); } /** * Construction tests. * * Construct LDIF object and see if we can get a handle. */ public function testConstruction() { $supported_modes = array('r', 'w', 'a'); $plus = array('', '+'); // Test all open modes, all of them should return a correct handle. foreach ($supported_modes as $mode) { foreach ($plus as $p) { $ldif = new Horde_Ldap_Ldif($this->_outfile, $mode, $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); } } // Test illegal option passing. try { $ldif = new Horde_Ldap_Ldif($this->_outfile, $mode, array('somebad' => 'option')); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Test passing custom handle. $handle = fopen($this->_outfile, 'r'); $ldif = new Horde_Ldap_Ldif($handle, $mode, $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); // Reading test with invalid file mode. try { $ldif = new Horde_Ldap_Ldif($this->_outfile, 'y', $this->_defaultConfig); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Reading test with non-existent file. try { $ldif = new Horde_Ldap_Ldif('some/nonexistent/file_for_net_ldap_ldif', 'r', $this->_defaultConfig); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Writing to non-existent file. $ldif = new Horde_Ldap_Ldif('testfile_for_net_ldap_ldif', 'w', $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); @unlink('testfile_for_net_ldap_ldif'); // Writing to non-existent path. try { $ldif = new Horde_Ldap_Ldif('some/nonexistent/file_for_net_ldap_ldif', 'w', $this->_defaultConfig); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} // Writing to existing file but without permission. chmod() should // succeed since we test that in setUp(). if (chmod($this->_outfile, 0444)) { try { $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $this->_defaultConfig); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} } else { $this->markTestSkipped('Could not chmod ' . $this->_outfile . ', write test without permission skipped'); } } /** * Tests if entries from an LDIF file are correctly constructed. */ public function testReadEntry() { /* UNIX line endings. */ $ldif = new Horde_Ldap_Ldif(__DIR__.'/fixtures/unsorted_w50.ldif', 'r', $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); $entries = array(); do { $entry = $ldif->readEntry(); $this->assertInstanceOf('Horde_Ldap_Entry', $entry); $entries[] = $entry; } while (!$ldif->eof()); $this->_compareEntries($this->_testentries, $entries); /* Windows line endings. */ $ldif = new Horde_Ldap_Ldif(__DIR__.'/fixtures/unsorted_w50_WIN.ldif', 'r', $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); $entries = array(); do { $entry = $ldif->readEntry(); $this->assertInstanceOf('Horde_Ldap_Entry', $entry); $entries[] = $entry; } while (!$ldif->eof()); $this->_compareEntries($this->_testentries, $entries); } /** * Tests if entries are correctly written. * * This tests converting entries to LDIF lines, wrapping, encoding, etc. */ public function testWriteEntry() { $testconf = $this->_defaultConfig; /* Test wrapped operation. */ $testconf['wrap'] = 50; $testconf['sort'] = 0; $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/unsorted_w50.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($this->_testentries); $ldif->done(); // Compare files. $this->assertEquals($expected, file($this->_outfile)); $testconf['wrap'] = 30; $testconf['sort'] = 0; $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/unsorted_w30.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($this->_testentries); $ldif->done(); // Compare files. $this->assertEquals($expected, file($this->_outfile)); /* Test unwrapped operation. */ $testconf['wrap'] = 40; $testconf['sort'] = 1; $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/sorted_w40.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($this->_testentries); $ldif->done(); // Compare files. $this->assertEquals($expected, file($this->_outfile)); $testconf['wrap'] = 50; $testconf['sort'] = 1; $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/sorted_w50.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($this->_testentries); $ldif->done(); // Compare files. $this->assertEquals($expected, file($this->_outfile)); /* Test raw option. */ $testconf['wrap'] = 50; $testconf['sort'] = 1; $testconf['raw'] = '/attr6/'; $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/sorted_w50.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($this->_testentries); $ldif->done(); // Compare files, with expected attributes adjusted. $this->assertEquals($expected, file($this->_outfile)); /* Test writing with non entry as parameter. */ $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w'); $this->assertTrue(is_resource($ldif->handle())); try { $ldif->writeEntry('malformed_parameter'); $this->fail('Horde_Ldap_Exception expected.'); } catch (Horde_Ldap_Exception $e) {} } /** * Test version writing. */ public function testWriteVersion() { $testconf = $this->_defaultConfig; $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/unsorted_w50.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); // Strip 1 additional line (the "version: 1" line that should not be // written now) and adjust test config. array_shift($expected); unset($testconf['version']); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($this->_testentries); $ldif->done(); // Compare files. $this->assertEquals($expected, file($this->_outfile)); } /** * Round trip test: Read LDIF, parse to entries, write that to LDIF and * compare both files. */ public function testReadWriteRead() { $ldif = new Horde_Ldap_Ldif(__DIR__.'/fixtures/unsorted_w50.ldif', 'r', $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); // Read LDIF. $entries = array(); do { $entry = $ldif->readEntry(); $this->assertInstanceOf('Horde_Ldap_Entry', $entry); $entries[] = $entry; } while (!$ldif->eof()); $ldif->done(); // Write LDIF. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $this->_defaultConfig); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($entries); $ldif->done(); // Compare files. $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/unsorted_w50.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); $this->assertEquals($expected, file($this->_outfile)); } /** * Tests if entry changes are correctly written. */ public function testWriteEntryChanges() { $testentries = $this->_testentries; $testentries[] = Horde_Ldap_Entry::createFresh('cn=foo,ou=example,dc=cno', array('cn' => 'foo')); $testentries[] = Horde_Ldap_Entry::createFresh('cn=footest,ou=example,dc=cno', array('cn' => 'foo')); $testconf = $this->_defaultConfig; $testconf['change'] = 1; /* No changes should produce empty file. */ $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($testentries); $ldif->done(); $this->assertEquals(array(), file($this->_outfile)); /* Changes test. */ // Prepare some changes. $testentries[0]->delete('attr1'); $testentries[0]->delete(array('attr2' => 'baz')); $testentries[0]->delete(array('attr4', 'attr3' => 'bar')); // Prepare some replaces and adds. $testentries[2]->replace(array('attr1' => 'newvaluefor1')); $testentries[2]->replace(array('attr2' => array('newvalue1for2', 'newvalue2for2'))); $testentries[2]->replace(array('attr3' => '')); $testentries[2]->replace(array('newattr' => 'foo')); // Delete whole entry. $testentries[3]->delete(); // Rename and move. $testentries[4]->dn('cn=Bar,ou=example,dc=cno'); $testentries[5]->dn('cn=foobartest,ou=newexample,dc=cno'); // Carry out write. $ldif = new Horde_Ldap_Ldif($this->_outfile, 'w', $testconf); $this->assertTrue(is_resource($ldif->handle())); $ldif->writeEntry($testentries); $ldif->done(); // Compare results. $expected = array_map(array($this, '_lineend'), file(__DIR__.'/fixtures/changes.ldif')); // Strip 4 starting lines because of comments in the file header. array_splice($expected, 0, 4); $this->assertEquals($expected, file($this->_outfile)); } /** * Test error dropping functionality. */ public function testError() { // No error. $ldif = new Horde_Ldap_Ldif(__DIR__ . '/fixtures/unsorted_w50.ldif', 'r', $this->_defaultConfig); // Test for line number reporting $ldif = new Horde_Ldap_Ldif(__DIR__ . '/fixtures/malformed_syntax.ldif', 'r', $this->_defaultConfig); $this->setExpectedException('Horde_Ldap_Exception', 'Invalid syntax at input line 7'); do { $entry = $ldif->readEntry(); } while (!$ldif->eof()); // Error giving error msg and line number: $this->setExpectedException('Horde_Ldap_Exception'); $ldif = new Horde_Ldap_Ldif(__DIR__ . '/some_not_existing/path/for/net_ldap_ldif', 'r', $this->_defaultConfig); } /** * Tests currentLines() and nextLines(). * * This should always return the same lines unless forced. */ public function testLineMethods() { $ldif = new Horde_Ldap_Ldif(__DIR__.'/fixtures/unsorted_w50.ldif', 'r', $this->_defaultConfig); $this->assertEquals(array(), $ldif->currentLines(), 'Horde_Ldap_Ldif initialization error!'); // Read first lines. $lines = $ldif->nextLines(); // Read the first lines several times and test. for ($i = 0; $i <= 10; $i++) { $r_lines = $ldif->nextLines(); $this->assertEquals($lines, $r_lines); } // Now force to iterate and see if the content changes. $r_lines = $ldif->nextLines(true); $this->assertNotEquals($lines, $r_lines); // It could be confusing to some people, but calling currentEntry() // would not work now, like the description of the method says. $no_entry = $ldif->currentLines(); $this->assertEquals(array(), $no_entry); } /** * Tests currentEntry(). This should always return the same object. */ public function testcurrentEntry() { $ldif = new Horde_Ldap_Ldif(__DIR__.'/fixtures/unsorted_w50.ldif', 'r', $this->_defaultConfig); // Read first entry. $entry = $ldif->readEntry(); // Test if currentEntry remains the first one. for ($i = 0; $i <= 10; $i++) { $e = $ldif->currentEntry(); $this->assertEquals($entry, $e); } } /** * Compares two Horde_Ldap_Entries. * * This helper function compares two entries (or array of entries) and * checks if they are equal. They are equal if all DNs from the first crowd * exist in the second AND each attribute is present and equal at the * respective entry. The search is case sensitive. * * @param array|Horde_Ldap_Entry $entry1 * @param array|Horde_Ldap_Entry $entry2 * @return boolean */ protected function _compareEntries($entry1, $entry2) { if (!is_array($entry1)) { $entry1 = array($entry1); } if (!is_array($entry2)) { $entry2 = array($entry2); } $entries_data1 = $entries_data2 = array(); // Step 1: extract and sort data. foreach ($entry1 as $e) { $values = $e->getValues(); foreach ($values as $attr_name => $attr_values) { if (!is_array($attr_values)) { $attr_values = array($attr_values); } $values[$attr_name] = $attr_values; } $entries_data1[$e->dn()] = $values; } foreach ($entry2 as $e) { $values = $e->getValues(); foreach ($values as $attr_name => $attr_values) { if (!is_array($attr_values)) { $attr_values = array($attr_values); } $values[$attr_name] = $attr_values; } $entries_data2[$e->dn()] = $values; } // Step 2: compare DNs (entries). $this->assertEquals(array_keys($entries_data1), array_keys($entries_data2), 'Entries DNs not equal! (missing entry or wrong DN)'); // Step 3: look for attribute existence and compare values. foreach ($entries_data1 as $dn => $attributes) { $this->assertEquals($entries_data1[$dn], $entries_data2[$dn], 'Entries ' . $dn . ' attributes are not equal'); foreach ($attributes as $attr_name => $attr_values) { $this->assertEquals(0, count(array_diff($entries_data1[$dn][$attr_name], $entries_data2[$dn][$attr_name])), 'Entries ' . $dn . ' attribute ' . $attr_name . ' values are not equal'); } } return true; } /** * Create line endings for current OS. * * This is neccessary to make write tests platform indendent. * * @param string $line Line * @return string */ protected function _lineend($line) { return rtrim($line) . PHP_EOL; } } Horde_Ldap-2.4.0/test/Horde/Ldap/phpunit.xml0000664000175000017500000000005613054777070016746 0ustar janjan Horde_Ldap-2.4.0/test/Horde/Ldap/SearchTest.php0000664000175000017500000001215713054777070017320 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_SearchTest extends Horde_Ldap_TestBase { public static function tearDownAfterClass() { if (!self::$ldapcfg) { return; } try { $ldap = new Horde_Ldap(self::$ldapcfg['server']); try { $ldap->delete('ou=Horde_Ldap_Test_search1,' . self::$ldapcfg['server']['basedn']); } catch (Exception $e) { } try { $ldap->delete('ou=Horde_Ldap_Test_search2,' . self::$ldapcfg['server']['basedn']); } catch (Exception $e) { } } catch (Exception $e) { } } /** * Tests SPL iterator. */ public function testSPLIterator() { $ldap = new Horde_Ldap(self::$ldapcfg['server']); // Some testdata, so we have some entries to search for. $base = self::$ldapcfg['server']['basedn']; $ou1 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_search1,' . $base, array( 'objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_search1')); $ou2 = Horde_Ldap_Entry::createFresh( 'ou=Horde_Ldap_Test_search2,' . $base, array( 'objectClass' => array('top', 'organizationalUnit'), 'ou' => 'Horde_Ldap_Test_search2')); $ldap->add($ou1); $this->assertTrue($ldap->exists($ou1->dn())); $ldap->add($ou2); $this->assertTrue($ldap->exists($ou2->dn())); /* Search and test each method. */ $search = $ldap->search(null, '(ou=Horde_Ldap*)'); $this->assertInstanceOf('Horde_Ldap_Search', $search); $this->assertEquals(2, $search->count()); // current() is supposed to return first valid element. $e1 = $search->current(); $this->assertInstanceOf('Horde_Ldap_Entry', $e1); $this->assertEquals($e1->dn(), $search->key()); $this->assertTrue($search->valid()); // Shift to next entry. $search->next(); $e2 = $search->current(); $this->assertInstanceOf('Horde_Ldap_Entry', $e2); $this->assertEquals($e2->dn(), $search->key()); $this->assertTrue($search->valid()); // Shift to non existent third entry. $search->next(); $this->assertFalse($search->current()); $this->assertFalse($search->key()); $this->assertFalse($search->valid()); // Rewind and test, which should return the first entry a second time. $search->rewind(); $e1_1 = $search->current(); $this->assertInstanceOf('Horde_Ldap_Entry', $e1_1); $this->assertEquals($e1_1->dn(), $search->key()); $this->assertTrue($search->valid()); $this->assertEquals($e1->dn(), $e1_1->dn()); // Don't rewind but call current, should return first entry again. $e1_2 = $search->current(); $this->assertInstanceOf('Horde_Ldap_Entry', $e1_2); $this->assertEquals($e1_2->dn(), $search->key()); $this->assertTrue($search->valid()); $this->assertEquals($e1->dn(), $e1_2->dn()); // Rewind again and test, which should return the first entry a third // time. $search->rewind(); $e1_3 = $search->current(); $this->assertInstanceOf('Horde_Ldap_Entry', $e1_3); $this->assertEquals($e1_3->dn(), $search->key()); $this->assertTrue($search->valid()); $this->assertEquals($e1->dn(), $e1_3->dn()); /* Try methods on empty search result. */ $search = $ldap->search(null, '(ou=Horde_LdapTest_NotExistentEntry)'); $this->assertInstanceOf('Horde_Ldap_Search', $search); $this->assertEquals(0, $search->count()); $this->assertFalse($search->current()); $this->assertFalse($search->key()); $this->assertFalse($search->valid()); $search->next(); $this->assertFalse($search->current()); $this->assertFalse($search->key()); $this->assertFalse($search->valid()); /* Search and simple iterate through the test entries. Then, rewind * and do it again several times. */ $search2 = $ldap->search(null, '(ou=Horde_Ldap*)'); $this->assertInstanceOf('Horde_Ldap_Search', $search2); $this->assertEquals(2, $search2->count()); for ($i = 0; $i <= 5; $i++) { $counter = 0; foreach ($search2 as $dn => $entry) { $counter++; // Check on type. $this->assertInstanceOf('Horde_Ldap_Entry', $entry); // Check on key. $this->assertThat(strlen($dn), $this->greaterThan(1)); $this->assertEquals($dn, $entry->dn()); } $this->assertEquals($search2->count(), $counter, "Failed at loop $i"); // Revert to start. $search2->rewind(); } } } Horde_Ldap-2.4.0/test/Horde/Ldap/TestBase.php0000664000175000017500000000135613054777070016764 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_TestBase extends Horde_Test_Case { protected static $ldapcfg; public function setUp() { // Check extension. try { Horde_Ldap::checkLDAPExtension(); } catch (Horde_Ldap_Exception $e) { $this->markTestSkipped($e->getMessage()); } $config = $this->getConfig('LDAP_TEST_CONFIG'); if (!$config) { $this->markTestSkipped('No configuration for LDAP tests.'); } self::$ldapcfg = $config; } } Horde_Ldap-2.4.0/test/Horde/Ldap/UtilTest.php0000664000175000017500000003250313054777070017025 0ustar janjan * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 */ class Horde_Ldap_UtilTest extends Horde_Test_Case { /** * Test escapeDNValue() */ public function testEscapeDNValue() { $dnval = ' ' . chr(22) . ' t,e+s"t,\\vl;u#e=! '; $expected = '\20\20\16 t\,e\+s\"t\,\\\\v\l\;u\#e\=!\20\20\20\20'; // String call. $this->assertEquals( array($expected), Horde_Ldap_Util::escapeDNValue($dnval)); // Array call. $this->assertEquals( array($expected), Horde_Ldap_Util::escapeDNValue(array($dnval))); // Multiple arrays. $this->assertEquals( array($expected, $expected, $expected), Horde_Ldap_Util::escapeDNValue(array($dnval, $dnval, $dnval))); } /** * Test unescapeDNValue() */ public function testUnescapeDNValue() { $dnval = '\\20\\20\\16\\20t\\,e\\+s \\"t\\,\\\\v\\l\\;u\\#e\\=!\\20\\20\\20\\20'; $expected = ' ' . chr(22) . ' t,e+s "t,\\vl;u#e=! '; // String call. $this->assertEquals( array($expected), Horde_Ldap_Util::unescapeDNValue($dnval)); // Array call. $this->assertEquals( array($expected), Horde_Ldap_Util::unescapeDNValue(array($dnval))); // Multiple arrays. $this->assertEquals( array($expected, $expected, $expected), Horde_Ldap_Util::unescapeDNValue(array($dnval, $dnval, $dnval))); } /** * Test escaping of filter values. */ public function testEscapeFilterValue() { $expected = 't\28e,s\29t\2av\5cal\1eue'; $filterval = 't(e,s)t*v\\al' . chr(30) . 'ue'; // String call $this->assertEquals( array($expected), Horde_Ldap_Util::escapeFilterValue($filterval)); // Array call. $this->assertEquals( array($expected), Horde_Ldap_Util::escapeFilterValue(array($filterval))); // Multiple arrays. $this->assertEquals( array($expected, $expected, $expected), Horde_Ldap_Util::escapeFilterValue(array($filterval, $filterval, $filterval))); } /** * Test unescaping of filter values. */ public function testUnescapeFilterValue() { $expected = 't(e,s)t*v\\al' . chr(30) . 'ue'; $filterval = 't\28e,s\29t\2av\5cal\1eue'; // String call $this->assertEquals( array($expected), Horde_Ldap_Util::unescapeFilterValue($filterval)); // Array call. $this->assertEquals( array($expected), Horde_Ldap_Util::unescapeFilterValue(array($filterval))); // Multiple arrays. $this->assertEquals( array($expected, $expected, $expected), Horde_Ldap_Util::unescapeFilterValue(array($filterval, $filterval, $filterval))); } /** * Test asc2hex32() */ public function testAsc2hex32() { $expected = '\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; $str = ''; for ($i = 0; $i < 127; $i++) { $str .= chr($i); } $this->assertEquals($expected, Horde_Ldap_Util::asc2hex32($str)); } /** * Test HEX unescaping */ public function testHex2asc() { $expected = ''; for ($i = 0; $i < 127; $i++) { $expected .= chr($i); } $str = '\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; $this->assertEquals($expected, Horde_Ldap_Util::hex2asc($str)); } /** * Tests splitRDNMultivalue() * * In addition to the above test of the basic split correction, we test * here the functionality of multivalued RDNs. */ public function testSplitRDNMultivalue() { // One value. $rdn = 'CN=J. Smith'; $expected = array('CN=J. Smith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Two values. $rdn = 'OU=Sales+CN=J. Smith'; $expected = array('OU=Sales', 'CN=J. Smith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Several multivals. $rdn = 'OU=Sales+CN=J. Smith+L=London+C=England'; $expected = array('OU=Sales', 'CN=J. Smith', 'L=London', 'C=England'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Unescaped "+" in value. $rdn = 'OU=Sa+les+CN=J. Smith'; $expected = array('OU=Sa+les', 'CN=J. Smith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Unescaped "+" in attr name. $rdn = 'O+U=Sales+CN=J. Smith'; $expected = array('O+U=Sales', 'CN=J. Smith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Unescaped "+" in attr name + value. $rdn = 'O+U=Sales+CN=J. Sm+ith'; $expected = array('O+U=Sales', 'CN=J. Sm+ith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Unescaped "+" in attribute name, but not first attribute. This // documents a known bug. However, unfortunately we can't know wether // the "C+" belongs to value "Sales" or attribute "C+N". To solve // this, we must ask the schema which we do not right now. The problem // is located in _correct_dn_splitting(). $rdn = 'OU=Sales+C+N=J. Smith'; // The "C+" is treaten as value of "OU". $expected = array('OU=Sales+C', 'N=J. Smith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); // Escaped "+" in attribute name and value. $rdn = 'O\+U=Sales+CN=J. Sm\+ith'; $expected = array('O\+U=Sales', 'CN=J. Sm\+ith'); $split = Horde_Ldap_Util::splitRDNMultivalue($rdn); $this->assertEquals($expected, $split); } /** * Tests attribute splitting ('foo=bar' => array('foo', 'bar')) */ public function testSplitAttributeString() { $attr_str = 'foo=bar'; // Properly. $expected = array('foo', 'bar'); $split = Horde_Ldap_Util::splitAttributeString($attr_str); $this->assertEquals($expected, $split); // Escaped "=". $attr_str = "fo\=o=b\=ar"; $expected = array('fo\=o', 'b\=ar'); $split = Horde_Ldap_Util::splitAttributeString($attr_str); $this->assertEquals($expected, $split); // Escaped "=" and unescaped = later on. $attr_str = "fo\=o=b=ar"; $expected = array('fo\=o', 'b=ar'); $split = Horde_Ldap_Util::splitAttributeString($attr_str); $this->assertEquals($expected, $split); } /** * Tests Ldap_explode_dn() */ public function testExplodeDN() { $dn = 'ou=Sales+CN=J. Smith,dc=example,dc=net'; $expected_casefold_none = array( array('CN=J. Smith', 'ou=Sales'), 'dc=example', 'dc=net' ); $expected_casefold_upper = array( array('CN=J. Smith', 'OU=Sales'), 'DC=example', 'DC=net' ); $expected_casefold_lower = array( array('cn=J. Smith', 'ou=Sales'), 'dc=example', 'dc=net' ); $expected_onlyvalues = array( array('J. Smith', 'Sales'), 'example', 'net' ); $expected_reverse = array_reverse($expected_casefold_upper); $dn_exploded_cnone = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'none')); $this->assertEquals($expected_casefold_none, $dn_exploded_cnone, 'Option casefold none failed'); $dn_exploded_cupper = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'upper')); $this->assertEquals($expected_casefold_upper, $dn_exploded_cupper, 'Option casefold upper failed'); $dn_exploded_clower = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'lower')); $this->assertEquals($expected_casefold_lower, $dn_exploded_clower, 'Option casefold lower failed'); $dn_exploded_onlyval = Horde_Ldap_Util::explodeDN($dn, array('onlyvalues' => true)); $this->assertEquals($expected_onlyvalues, $dn_exploded_onlyval, 'Option onlyval failed'); $dn_exploded_reverse = Horde_Ldap_Util::explodeDN($dn, array('reverse' => true)); $this->assertEquals($expected_reverse, $dn_exploded_reverse, 'Option reverse failed'); $this->assertEquals( array('CN=J\\, Smith', 'DC=example', 'DC=net'), Horde_Ldap_Util::explodeDN('cn=J\\, Smith,dc=example,dc=net')); } /** * 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'); } } }