package.xml 0000664 0001750 0001750 00000044032 12653737604 011317 0 ustar jan jan
Horde_Ldappear.horde.orgHorde_LDAPHorde LDAP librariesA set of classes for connecting to LDAP servers and working with directory objects.Ben Klangbklangben@alkaloid.netyesJan Schneiderjanjan@horde.orgyes2016-02-012.3.21.3.0stablestableLGPL-3.0
* [jan] Mark PHP 7 as supported.
5.3.08.0.0alpha18.0.0alpha11.7.0Horde_Exceptionpear.horde.org2.0.03.0.0alpha13.0.0alpha1Horde_Utilpear.horde.org2.0.03.0.0alpha13.0.0alpha1ldapHorde_Testpear.horde.org2.1.03.0.0alpha13.0.0alpha11.0.0alpha11.0.0alphaalpha2011-03-08LGPLv3
* 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.0beta11.0.0betabeta2011-03-16LGPL-3.0
* First beta release for Horde 4.
1.0.0RC11.0.0betabeta2011-03-22LGPL-3.0
* First release candidate for Horde 4.
1.0.0RC21.0.0betabeta2011-03-29LGPL-3.0
* Second release candidate for Horde 4.
1.0.01.0.0stablestable2011-04-06LGPL-3.0
* First stable release for Horde 4.
1.1.01.1.0stablestable2011-04-20LGPL-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.11.1.0stablestable2011-05-03LGPL-3.0
* [jan] Fix detecting superiour objectclass' must/may attributes (Bug #9826).
1.1.21.1.0stablestable2011-07-05LGPL-3.0
* [jan] Always use negative lookbehind assertions to work around bug in PCRE 6.6 (Steve Teti, Bug #10294).
1.1.31.1.0stablestable2011-08-30LGPL-3.0
* [jan] Speed up Horde_Ldap_Search#shiftEntry() on large result sets.
1.1.41.1.0stablestable2011-11-08LGPL-3.0
* [jan] Add missing test autoloader.
1.1.51.1.0stablestable2012-05-01LGPL-3.0
* [jan] Throw exception if LDAP extension is missing.
2.0.0alpha11.1.0alphastable2012-07-05LGPL-3.0
* First alpha release for Horde 5.
2.0.0beta11.1.0betastable2012-07-19LGPL-3.0
* First beta release for Horde 5.
2.0.01.1.0stablestable2012-10-30LGPL-3.0
* First stable release for Horde 5.
2.0.11.1.0stablestable2012-11-19LGPL-3.0
* [mms] Use new Horde_Test layout.
2.0.21.1.0stablestable2013-01-29LGPL-3.0
* [jan] Replace preg_replace() /e modifier.
2.0.31.1.0stablestable2013-10-28LGPL-3.0
* [jan] Fix modifying entries with modify().
* [jan] Try starting TLS without querying the rootDSE (Bug #12157).
2.0.41.1.0stablestable2014-04-03LGPL-3.0
* [jan] Fix approximate search operator (Leandro Damascena <leandro.damascena@gmail.com>, Bug #9094).
2.0.51.1.0stablestable2014-05-21LGPL-3.0
* [jan] Fix creating filters with the less (<) operator (Bug #13154).
2.0.61.1.0stablestable2014-06-03LGPL-3.0
* [jan] SECURITY: Stricter parameter check in bind() to detect empty passwords.
2.1.01.1.1stablestable2014-06-10LGPL-3.0
* [jan] Support multi-value RDNs in Horde_Ldap::quoteDN() (Request #11888).
2.2.01.2.0stablestable2014-06-17LGPL-3.0
* [jan] Fix compatibility with PHP 5.3 (Bug #11888).
* [jan] Allow to specify base DN for searching user DNs.
2.3.01.3.0stablestable2015-02-10LGPL-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.11.3.0stablestable2015-02-12LGPL-3.0
* [jan] Fix connection if using ldaps:// scheme (Bug #13858).
2.3.21.3.0stablestable2016-02-01LGPL-3.0
* [jan] Mark PHP 7 as supported.
Horde_Ldap-2.3.2/doc/Horde/Ldap/COPYING 0000664 0001750 0001750 00000016743 12653737604 015374 0 ustar jan jan 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.3.2/lib/Horde/Ldap/Entry.php 0000664 0001750 0001750 00000073737 12653737604 016162 0 ustar jan jan
* @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.3.2/lib/Horde/Ldap/Exception.php 0000664 0001750 0001750 00000000644 12653737604 017002 0 ustar jan jan
*/
class Horde_Ldap_Exception extends Horde_Exception_Wrapped
{
}
Horde_Ldap-2.3.2/lib/Horde/Ldap/Filter.php 0000664 0001750 0001750 00000040076 12653737604 016274 0 ustar jan jan
* $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-2016 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.3.2/lib/Horde/Ldap/Ldif.php 0000664 0001750 0001750 00000070015 12653737604 015721 0 ustar jan jan
* // 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-2016 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.3.2/lib/Horde/Ldap/RootDse.php 0000664 0001750 0001750 00000010152 12653737604 016416 0 ustar jan jan
* @author Jan Schneider
* @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
*/
class Horde_Ldap_RootDse
{
/**
* @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->get_value($attr, 'all'))) {
return false;
}
}
return true;
}
}
Horde_Ldap-2.3.2/lib/Horde/Ldap/Schema.php 0000664 0001750 0001750 00000037774 12653737604 016262 0 ustar jan jan
* @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.3.2/lib/Horde/Ldap/Search.php 0000664 0001750 0001750 00000036564 12653737604 016263 0 ustar jan jan
* @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.3.2/lib/Horde/Ldap/Util.php 0000664 0001750 0001750 00000055762 12653737604 015774 0 ustar jan jan
* @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('/.+(?