Files
server/usr/share/psa-pear/pear/php/Horde/Ldap/Entry.php
2026-01-07 20:52:11 +01:00

894 lines
30 KiB
PHP

<?php
/**
* Horde_Ldap_Entry represents an LDAP entry.
*
* Copyright 2003-2007 Tarjej Huse, Jan Wagner, Benedikt Hallinger
* Copyright 2009-2017 Horde LLC (http://www.horde.org/)
*
* @package Ldap
* @author Jan Wagner <wagner@netsols.de>
* @author Tarjej Huse <tarjei@bergfald.no>
* @author Benedikt Hallinger <beni@php.net>
* @author Ben Klang <ben@alkaloid.net>
* @author Jan Schneider <jan@horde.org>
* @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:
* <code>
* $attrs = array('attribute1' => array('value1', 'value2'),
* 'attribute2' => 'single value');
* </code>
*
* @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:
* <code>
* $attrs = array('attribute1' => array('value1', 'value2'),
* 'attribute2' => 'single value');
* </code>
*
* @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
* <code>
* array('attributename' => 'single value',
* 'attributename' => array('value1', value2', value3'))
* </code>
*
* @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:
* <code>
* array('attributename' => 'single value',
* 'attributename' => array('value1', 'value2'))
* </code>
*
* 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:
* <code>
* array('attributename' => 'single value',
* 'attribute2name' => array('value1', 'value2'),
* 'deleteme1' => null,
* 'deleteme2' => '')
* </code>
*
* 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):
* <code>
* $matches = array(
* 0 => array (usual preg_match() returned array),
* 1 => array (usual preg_match() returned array)
* )
* </code>
* $matches will always be initialized to an empty array inside.
*
* Usage example:
* <code>
* 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();
* }
* </code>
*
* @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;
}
}