1<?php
2/**
3 * Horde_Ldap_Entry represents an LDAP entry.
4 *
5 * Copyright 2003-2007 Tarjej Huse, Jan Wagner, Benedikt Hallinger
6 * Copyright 2009-2017 Horde LLC (http://www.horde.org/)
7 *
8 * @package   Ldap
9 * @author    Jan Wagner <wagner@netsols.de>
10 * @author    Tarjej Huse <tarjei@bergfald.no>
11 * @author    Benedikt Hallinger <beni@php.net>
12 * @author    Ben Klang <ben@alkaloid.net>
13 * @author    Jan Schneider <jan@horde.org>
14 * @license   http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
15 */
16class Horde_Ldap_Entry
17{
18    /**
19     * Entry resource identifier.
20     *
21     * @var resource
22     */
23    protected $_entry;
24
25    /**
26     * LDAP resource identifier.
27     *
28     * @var resource
29     */
30    protected $_link;
31
32    /**
33     * Horde_Ldap object.
34     *
35     * This object will be used for updating and schema checking.
36     *
37     * @var Horde_Ldap
38     */
39    protected $_ldap;
40
41    /**
42     * Distinguished name of the entry.
43     *
44     * @var string
45     */
46    protected $_dn;
47
48    /**
49     * Attributes.
50     *
51     * @var array
52     */
53    protected $_attributes = array();
54
55    /**
56     * Original attributes before any modification.
57     *
58     * @var array
59     */
60    protected $_original = array();
61
62    /**
63     * Map of attribute names.
64     *
65     * @var array
66     */
67    protected $_map = array();
68
69    /**
70     * Is this a new entry?
71     *
72     * @var boolean
73     */
74    protected $_new = true;
75
76    /**
77     * New distinguished name.
78     *
79     * @var string
80     */
81    protected $_newdn;
82
83    /**
84     * Shall the entry be deleted?
85     *
86     * @var boolean
87     */
88    protected $_delete = false;
89
90    /**
91     * Map with changes to the entry.
92     *
93     * @var array
94     */
95    protected $_changes = array('add'     => array(),
96                                'delete'  => array(),
97                                'replace' => array());
98
99    /**
100     * Constructor.
101     *
102     * Sets up the distinguished name and the entries attributes.
103     *
104     * Use {@link Horde_Ldap_Entry::createFresh()} or {@link
105     * Horde_Ldap_Entry::createConnected()} to create Horde_Ldap_Entry objects.
106     *
107     * @param Horde_Ldap|resource|array $ldap Horde_Ldap object, LDAP
108     *                                        connection resource or
109     *                                        array of attributes.
110     * @param string|resource          $entry Either a DN or a LDAP entry
111     *                                        resource.
112     */
113    protected function __construct($ldap, $entry = null)
114    {
115        /* Set up entry resource or DN. */
116        if (is_resource($entry)) {
117            $this->_entry = $entry;
118        } else {
119            $this->_dn = $entry;
120        }
121
122        /* Set up LDAP link. */
123        if ($ldap instanceof Horde_Ldap) {
124            $this->_ldap = $ldap;
125            $this->_link = $ldap->getLink();
126        } elseif (is_resource($ldap)) {
127            $this->_link = $ldap;
128        } elseif (is_array($ldap)) {
129            /* Special case: here $ldap is an array of attributes, this means,
130             * we have no link. This is a "virtual" entry.  We just set up the
131             * attributes so one can work with the object as expected, but an
132             * update() fails unless setLDAP() is called. */
133            $this->_loadAttributes($ldap);
134        }
135
136        /* If this is an entry existing in the directory, then set up as old
137         * and fetch attributes. */
138        if (is_resource($this->_entry) && is_resource($this->_link)) {
139            $this->_new = false;
140            $this->_dn  = @ldap_get_dn($this->_link, $this->_entry);
141            /* Fetch attributes from server. */
142            $this->_loadAttributes();
143        }
144    }
145
146    /**
147     * Creates a fresh entry that may be added to the directory later.
148     *
149     * You should put a 'objectClass' attribute into the $attrs so the
150     * directory server knows which object you want to create. However, you may
151     * omit this in case you don't want to add this entry to a directory
152     * server.
153     *
154     * The attributes parameter is as following:
155     * <code>
156     * $attrs = array('attribute1' => array('value1', 'value2'),
157     *                'attribute2' => 'single value');
158     * </code>
159     *
160     * @param string $dn    DN of the entry.
161     * @param array  $attrs Attributes of the entry.
162     *
163     * @return Horde_Ldap_Entry
164     * @throws Horde_Ldap_Exception
165     */
166    public static function createFresh($dn, array $attrs = array())
167    {
168        return new Horde_Ldap_Entry($attrs, $dn);
169    }
170
171    /**
172     * Creates an entry object out of an LDAP entry resource.
173     *
174     * Use this method, if you want to initialize an entry object that is
175     * already present in some directory and that you have read manually.
176     *
177     * @param Horde_Ldap $ldap Horde_Ldap object.
178     * @param resource  $entry PHP LDAP entry resource.
179     *
180     * @return Horde_Ldap_Entry
181     * @throws Horde_Ldap_Exception
182     */
183    public static function createConnected(Horde_Ldap $ldap, $entry)
184    {
185        if (!is_resource($entry)) {
186            throw new Horde_Ldap_Exception('Unable to create connected entry: Parameter $entry needs to be a ldap entry resource!');
187        }
188
189        return new Horde_Ldap_Entry($ldap, $entry);
190    }
191
192    /**
193     * Creates an entry object that is considered to exist already.
194     *
195     * Use this method, if you want to modify an already existing entry without
196     * fetching it first.  In most cases however, it is better to fetch the
197     * entry via Horde_Ldap::getEntry().
198     *
199     * You should take care if you construct entries manually with this because
200     * you may get weird synchronisation problems.  The attributes and values
201     * as well as the entry itself are considered existent which may produce
202     * errors if you try to modify an entry which doesn't really exist or if
203     * you try to overwrite some attribute with an value already present.
204     *
205     * The attributes parameter is as following:
206     * <code>
207     * $attrs = array('attribute1' => array('value1', 'value2'),
208     *                'attribute2' => 'single value');
209     * </code>
210     *
211     * @param string $dn    DN of the entry.
212     * @param array  $attrs Attributes of the entry.
213     *
214     * @return Horde_Ldap_Entry
215     * @throws Horde_Ldap_Exception
216     */
217    public static function createExisting($dn, array $attrs = array())
218    {
219        $entry = self::createFresh($dn, $attrs);
220        $entry->markAsNew(false);
221        return $entry;
222    }
223
224    /**
225     * Returns or sets the distinguished name of the entry.
226     *
227     * If called without an argument the current (or the new DN if set) DN gets
228     * returned.
229     *
230     * If you provide an DN, this entry is moved to the new location specified
231     * if a DN existed.
232     *
233     * If the DN was not set, the DN gets initialized. Call {@link update()} to
234     * actually create the new entry in the directory.
235     *
236     * To fetch the current active DN after setting a new DN but before an
237     * update(), you can use {@link currentDN()} to retrieve the DN that is
238     * currently active.
239     *
240     * @todo expect utf-8 data.
241     * Please note that special characters (eg german umlauts) should be encoded using utf8_encode().
242     * You may use {@link Horde_Ldap_Util::canonicalDN()} for properly encoding of the DN.
243     *
244     * @param string $dn New distinguished name.
245     *
246     * @return string Distinguished name.
247     */
248    public function dn($dn = null)
249    {
250        if (!is_null($dn)) {
251            if (is_null($this->_dn)) {
252                $this->_dn = $dn;
253            } else {
254                $this->_newdn = $dn;
255            }
256            return $dn;
257        }
258        return isset($this->_newdn) ? $this->_newdn : $this->currentDN();
259    }
260
261    /**
262     * Sets the internal attributes array.
263     *
264     * This method fetches the values for the attributes from the server.  The
265     * attribute syntax will be checked so binary attributes will be returned
266     * as binary values.
267     *
268     * Attributes may be passed directly via the $attributes parameter to setup
269     * this entry manually. This overrides attribute fetching from the server.
270     *
271     * @param array $attributes Attributes to set for this entry.
272     */
273    protected function _loadAttributes(array $attributes = null)
274    {
275        /* Fetch attributes from the server. */
276        if (is_null($attributes) &&
277            is_resource($this->_entry) &&
278            is_resource($this->_link)) {
279            /* Fetch schema. */
280            if ($this->_ldap instanceof Horde_Ldap) {
281                try {
282                    $schema = $this->_ldap->schema();
283                } catch (Horde_Ldap_Exception $e) {
284                    $schema = null;
285                }
286            }
287
288            /* Fetch attributes. */
289            $attributes = array();
290            for ($attr = @ldap_first_attribute($this->_link, $this->_entry);
291                 $attr;
292                 $attr = @ldap_next_attribute($this->_link, $this->_entry)) {
293                /* Standard function to fetch value. */
294                $func = 'ldap_get_values';
295
296                /* Try to get binary values as binary data. */
297                if ($schema instanceof Horde_Ldap_Schema &&
298                    $schema->isBinary($attr)) {
299                    $func = 'ldap_get_values_len';
300                }
301
302                /* Fetch attribute value (needs error checking?) . */
303                $attributes[$attr] = $func($this->_link, $this->_entry, $attr);
304            }
305        }
306
307        /* Set attribute data directly, if passed. */
308        if (is_array($attributes) && count($attributes) > 0) {
309            if (isset($attributes['count']) &&
310                is_numeric($attributes['count'])) {
311                unset($attributes['count']);
312            }
313            foreach ($attributes as $k => $v) {
314                /* Attribute names should not be numeric. */
315                if (is_numeric($k)) {
316                    continue;
317                }
318
319                /* Map generic attribute name to real one. */
320                $this->_map[Horde_String::lower($k)] = $k;
321
322                /* Attribute values should be in an array. */
323                if (false == is_array($v)) {
324                    $v = array($v);
325                }
326
327                /* Remove the value count (comes from LDAP server). */
328                if (isset($v['count'])) {
329                    unset($v['count']);
330                }
331                $this->_attributes[$k] = $v;
332            }
333        }
334
335        /* Save a copy for later use. */
336        $this->_original = $this->_attributes;
337    }
338
339    /**
340     * Returns the values of all attributes in a hash.
341     *
342     * The returned hash has the form
343     * <code>
344     * array('attributename' => 'single value',
345     *       'attributename' => array('value1', value2', value3'))
346     * </code>
347     *
348     * @return array Hash of all attributes with their values.
349     * @throws Horde_Ldap_Exception
350     */
351    public function getValues()
352    {
353        $attrs = array();
354        foreach (array_keys($this->_attributes) as $attr) {
355            $attrs[$attr] = $this->getValue($attr);
356        }
357        return $attrs;
358    }
359
360    /**
361     * Returns the value of a specific attribute.
362     *
363     * The first parameter is the name of the attribute.
364     *
365     * The second parameter influences the way the value is returned:
366     * - 'single': only the first value is returned as string.
367     * - 'all': all values are returned in an array.
368     * In all other cases an attribute value with a single value is returned as
369     * string, if it has multiple values it is returned as an array.
370     *
371     * @param string $attr   Attribute name.
372     * @param string $option Option.
373     *
374     * @return string|array Attribute value(s).
375     * @throws Horde_Ldap_Exception
376     */
377    public function getValue($attr, $option = null)
378    {
379        $attr = $this->_getAttrName($attr);
380
381        if (!array_key_exists($attr, $this->_attributes)) {
382            throw new Horde_Ldap_Exception('Unknown attribute (' . $attr . ') requested');
383        }
384
385        $value = $this->_attributes[$attr];
386
387        if ($option == 'single' || (count($value) == 1 && $option != 'all')) {
388            $value = array_shift($value);
389        }
390
391        return $value;
392    }
393
394    /**
395     * Returns an array of attributes names.
396     *
397     * @return array Array of attribute names.
398     */
399    public function attributes()
400    {
401        return array_keys($this->_attributes);
402    }
403
404    /**
405     * Returns whether an attribute exists or not.
406     *
407     * @param string $attr Attribute name.
408     *
409     * @return boolean True if the attribute exists.
410     */
411    public function exists($attr)
412    {
413        $attr = $this->_getAttrName($attr);
414        return array_key_exists($attr, $this->_attributes);
415    }
416
417    /**
418     * Adds new attributes or a new values to existing attributes.
419     *
420     * The paramter has to be an array of the form:
421     * <code>
422     * array('attributename' => 'single value',
423     *       'attributename' => array('value1', 'value2'))
424     * </code>
425     *
426     * When the attribute already exists the values will be added, otherwise
427     * the attribute will be created. These changes are local to the entry and
428     * do not affect the entry on the server until update() is called.
429     *
430     * You can add values of attributes that you haven't originally selected,
431     * but if you do so, {@link getValue()} and {@link getValues()} will only
432     * return the values you added, *NOT* all values present on the server. To
433     * avoid this, just refetch the entry after calling {@link update()} or
434     * select the attribute.
435     *
436     * @param array $attr Attributes to add.
437     */
438    public function add(array $attr = array())
439    {
440        foreach ($attr as $k => $v) {
441            $k = $this->_getAttrName($k);
442            if (!is_array($v)) {
443                /* Do not add empty values. */
444                if ($v == null) {
445                    continue;
446                } else {
447                    $v = array($v);
448                }
449            }
450
451            /* Add new values to existing attribute or add new attribute. */
452            if ($this->exists($k)) {
453                $this->_attributes[$k] = array_unique(array_merge($this->_attributes[$k], $v));
454            } else {
455                $this->_map[Horde_String::lower($k)] = $k;
456                $this->_attributes[$k]      = $v;
457            }
458
459            /* Save changes for update(). */
460            if (empty($this->_changes['add'][$k])) {
461                $this->_changes['add'][$k] = array();
462            }
463            $this->_changes['add'][$k] = array_unique(array_merge($this->_changes['add'][$k], $v));
464        }
465    }
466
467    /**
468     * Deletes an attribute, a value or the whole entry.
469     *
470     * The parameter can be one of the following:
471     *
472     * - 'attributename': the attribute as a whole will be deleted.
473     * - array('attributename1', 'attributename2'): all specified attributes
474     *                                              will be deleted.
475     * - array('attributename' => 'value'): the specified attribute value will
476     *                                      be deleted.
477     * - array('attributename' => array('value1', 'value2'): The specified
478     *                                                       attribute values
479     *                                                       will be deleted.
480     * - null: the whole entry will be deleted.
481     *
482     * These changes are local to the entry and do not affect the entry on the
483     * server until {@link update()} is called.
484     *
485     * You must select the attribute (at $ldap->search() for example) to be
486     * able to delete values of it, Otherwise {@link update()} will silently
487     * fail and remove nothing.
488     *
489     * @param string|array $attr Attributes to delete.
490     */
491    public function delete($attr = null)
492    {
493        if (is_null($attr)) {
494            $this->_delete = true;
495            return;
496        }
497
498        if (is_string($attr)) {
499            $attr = array($attr);
500        }
501
502        /* Make the assumption that attribute names cannot be numeric,
503         * therefore this has to be a simple list of attribute names to
504         * delete. */
505        reset($attr);
506        if (is_numeric(key($attr))) {
507            foreach ($attr as $name) {
508                if (is_array($name)) {
509                    /* Mixed modes (list mode but specific values given!). */
510                    $del_attr_name = array_search($name, $attr);
511                    $this->delete(array($del_attr_name => $name));
512                } else {
513                    /* Mark for update() if this attribute was not marked
514                     before. */
515                    $name = $this->_getAttrName($name);
516                    if ($this->exists($name)) {
517                        $this->_changes['delete'][$name] = null;
518                        unset($this->_attributes[$name]);
519                    }
520                }
521            }
522        } else {
523            /* We have a hash with 'attributename' => 'value to delete'. */
524            foreach ($attr as $name => $values) {
525                if (is_int($name)) {
526                    /* Mixed modes and gave us just an attribute name. */
527                    $this->delete($values);
528                } else {
529                    /* Mark for update() if this attribute was not marked
530                     * before; this time it must consider the selected values
531                     * too. */
532                    $name = $this->_getAttrName($name);
533                    if ($this->exists($name)) {
534                        if (!is_array($values)) {
535                            $values = array($values);
536                        }
537                        /* Save values to be deleted. */
538                        if (empty($this->_changes['delete'][$name])) {
539                            $this->_changes['delete'][$name] = array();
540                        }
541                        $this->_changes['delete'][$name] =
542                            array_unique(array_merge($this->_changes['delete'][$name], $values));
543                        foreach ($values as $value) {
544                            /* Find the key for the value that should be
545                             * deleted. */
546                            $key = array_search($value, $this->_attributes[$name]);
547                            if (false !== $key) {
548                                /* Delete the value. */
549                                unset($this->_attributes[$name][$key]);
550                            }
551                        }
552                    }
553                }
554            }
555        }
556    }
557
558    /**
559     * Replaces attributes or their values.
560     *
561     * The parameter has to an array of the following form:
562     * <code>
563     * array('attributename' => 'single value',
564     *       'attribute2name' => array('value1', 'value2'),
565     *       'deleteme1' => null,
566     *       'deleteme2' => '')
567     * </code>
568     *
569     * If the attribute does not yet exist it will be added instead (see also
570     * $force). If the attribue value is null, the attribute will de deleted.
571     *
572     * These changes are local to the entry and do not affect the entry on the
573     * server until {@link update()} is called.
574     *
575     * In some cases you are not allowed to read the attributes value (for
576     * example the ActiveDirectory attribute unicodePwd) but are allowed to
577     * replace the value. In this case replace() would assume that the
578     * attribute is not in the directory yet and tries to add it which will
579     * result in an LDAP_TYPE_OR_VALUE_EXISTS error. To force replace mode
580     * instead of add, you can set $force to true.
581     *
582     * @param array   $attr  Attributes to replace.
583     * @param boolean $force Force replacing mode in case we can't read the
584     *                       attribute value but are allowed to replace it.
585     */
586    public function replace(array $attr = array(), $force = false)
587    {
588        foreach ($attr as $k => $v) {
589            $k = $this->_getAttrName($k);
590            if (!is_array($v)) {
591                /* Delete attributes with empty values; treat integers as
592                 * string. */
593                if (is_int($v)) {
594                    $v = (string)$v;
595                }
596                if ($v == null) {
597                    $this->delete($k);
598                    continue;
599                } else {
600                    $v = array($v);
601                }
602            }
603            /* Existing attributes will get replaced. */
604            if ($this->exists($k) || $force) {
605                $this->_changes['replace'][$k] = $v;
606                $this->_attributes[$k]         = $v;
607            } else {
608                /* New ones just get added. */
609                $this->add(array($k => $v));
610            }
611        }
612    }
613
614    /**
615     * Updates the entry on the directory server.
616     *
617     * This will evaluate all changes made so far and send them to the
618     * directory server.
619     *
620     * If you make changes to objectclasses wich have mandatory attributes set,
621     * update() will currently fail. Remove the entry from the server and readd
622     * it as new in such cases. This also will deal with problems with setting
623     * structural object classes.
624     *
625     * @todo Entry rename with a DN containing special characters needs testing!
626     *
627     * @throws Horde_Ldap_Exception
628     */
629    public function update()
630    {
631        /* Ensure we have a valid LDAP object. */
632        $ldap = $this->getLDAP();
633
634        /* Get and check link. */
635        $link = $ldap->getLink();
636        if (!is_resource($link)) {
637            throw new Horde_Ldap_Exception('Could not update entry: internal LDAP link is invalid');
638        }
639
640        /* Delete the entry. */
641        if ($this->_delete) {
642            return $ldap->delete($this);
643        }
644
645        /* New entry. */
646        if ($this->_new) {
647            $ldap->add($this);
648            $this->_new                = false;
649            $this->_changes['add']     = array();
650            $this->_changes['delete']  = array();
651            $this->_changes['replace'] = array();
652            $this->_original           = $this->_attributes;
653            return;
654        }
655
656        /* Rename/move entry. */
657        if (!is_null($this->_newdn)) {
658            if ($ldap->getVersion() != 3) {
659                throw new Horde_Ldap_Exception('Renaming/Moving an entry is only supported in LDAPv3');
660            }
661            /* Make DN relative to parent (needed for LDAP rename). */
662            $parent = Horde_Ldap_Util::explodeDN($this->_newdn, array('casefolding' => 'none', 'reverse' => false, 'onlyvalues' => false));
663            $child = array_shift($parent);
664
665            /* Maybe the DN consist of a multivalued RDN, we must build the DN
666             * in this case because the $child RDN is an array. */
667            if (is_array($child)) {
668                $child = Horde_Ldap_Util::canonicalDN($child);
669            }
670            $parent = Horde_Ldap_Util::canonicalDN($parent);
671
672            /* Rename/move. */
673            if (!@ldap_rename($link, $this->_dn, $child, $parent, true)) {
674                throw new Horde_Ldap_Exception('Entry not renamed: ' . @ldap_error($link), @ldap_errno($link));
675            }
676
677            /* Reflect changes to local copy. */
678            $this->_dn    = $this->_newdn;
679            $this->_newdn = null;
680        }
681
682        /* Carry out modifications to the entry. */
683        foreach ($this->_changes['add'] as $attr => $value) {
684            /* If attribute exists, add new values. */
685            if ($this->exists($attr)) {
686                if (!@ldap_mod_add($link, $this->dn(), array($attr => $value))) {
687                    throw new Horde_Ldap_Exception('Could not add new values to attribute ' . $attr . ': ' . @ldap_error($link), @ldap_errno($link));
688                }
689            } else {
690                /* New attribute. */
691                if (!@ldap_modify($link, $this->dn(), array($attr => $value))) {
692                    throw new Horde_Ldap_Exception('Could not add new attribute ' . $attr . ': ' . @ldap_error($link), @ldap_errno($link));
693                }
694            }
695            unset($this->_changes['add'][$attr]);
696        }
697
698        foreach ($this->_changes['delete'] as $attr => $value) {
699            /* In LDAPv3 you need to specify the old values for deleting. */
700            if (is_null($value) && $ldap->getVersion() == 3) {
701                $value = $this->_original[$attr];
702            }
703            if (!@ldap_mod_del($link, $this->dn(), array($attr => $value))) {
704                throw new Horde_Ldap_Exception('Could not delete attribute ' . $attr . ': ' . @ldap_error($link), @ldap_errno($link));
705            }
706            unset($this->_changes['delete'][$attr]);
707        }
708
709        foreach ($this->_changes['replace'] as $attr => $value) {
710            if (!@ldap_modify($link, $this->dn(), array($attr => $value))) {
711                throw new Horde_Ldap_Exception('Could not replace attribute ' . $attr . ' values: ' . @ldap_error($link), @ldap_errno($link));
712            }
713            unset($this->_changes['replace'][$attr]);
714        }
715
716        /* All went well, so $_attributes (local copy) becomes $_original
717         * (server). */
718        $this->_original = $this->_attributes;
719    }
720
721    /**
722     * Returns the right attribute name.
723     *
724     * @param string $attr Name of attribute.
725     *
726     * @return string The right name of the attribute
727     */
728    protected function _getAttrName($attr)
729    {
730        $name = Horde_String::lower($attr);
731        return isset($this->_map[$name]) ? $this->_map[$name] : $attr;
732    }
733
734    /**
735     * Returns a reference to the LDAP-Object of this entry.
736     *
737     * @return Horde_Ldap  Reference to the Horde_Ldap object (the connection).
738     * @throws Horde_Ldap_Exception
739     */
740    public function getLDAP()
741    {
742        if (!($this->_ldap instanceof Horde_Ldap)) {
743            throw new Horde_Ldap_Exception('ldap property is not a valid Horde_Ldap object');
744        }
745        return $this->_ldap;
746    }
747
748    /**
749     * Sets a reference to the LDAP object of this entry.
750     *
751     * After setting a Horde_Ldap object, calling update() will use that object
752     * for updating directory contents. Use this to dynamicly switch
753     * directories.
754     *
755     * @param Horde_Ldap $ldap  Horde_Ldap object that this entry should be
756     *                          connected to.
757     *
758     * @throws Horde_Ldap_Exception
759     */
760    public function setLDAP(Horde_Ldap $ldap)
761    {
762        $this->_ldap = $ldap;
763    }
764
765    /**
766     * Marks the entry as new or existing.
767     *
768     * If an entry is marked as new, it will be added to the directory when
769     * calling {@link update()}.
770     *
771     * If the entry is marked as old ($mark = false), then the entry is assumed
772     * to be present in the directory server wich results in modification when
773     * calling {@link update()}.
774     *
775     * @param boolean $mark Whether to mark the entry as new.
776     */
777    public function markAsNew($mark = true)
778    {
779        $this->_new = (bool)$mark;
780    }
781
782    /**
783     * Applies a regular expression onto a single- or multi-valued attribute
784     * (like preg_match()).
785     *
786     * This method behaves like PHP's preg_match() but with some exception.
787     * Since it is possible to have multi valued attributes the $matches
788     * array will have a additionally numerical dimension (one for each value):
789     * <code>
790     * $matches = array(
791     *         0 => array (usual preg_match() returned array),
792     *         1 => array (usual preg_match() returned array)
793     * )
794     * </code>
795     * $matches will always be initialized to an empty array inside.
796     *
797     * Usage example:
798     * <code>
799     * try {
800     *     if ($entry->pregMatch('/089(\d+)/', 'telephoneNumber', $matches)) {
801     *         // Match of value 1, content of first bracket
802     *         echo 'First match: ' . $matches[0][1];
803     *     } else {
804     *         echo 'No match found.';
805     *     }
806     * } catch (Horde_Ldap_Exception $e) {
807     *     echo 'Error: ' . $e->getMessage();
808     * }
809     * </code>
810     *
811     * @param string $regex     The regular expression.
812     * @param string $attr_name The attribute to search in.
813     * @param array  $matches   Array to store matches in.
814     *
815     * @return boolean  True if we had a match in one of the values.
816     * @throws Horde_Ldap_Exception
817     */
818    public function pregMatch($regex, $attr_name, &$matches = array())
819    {
820        /* Fetch attribute values. */
821        $attr = $this->getValue($attr_name, 'all');
822        unset($attr['count']);
823
824        /* Perform preg_match() on all values. */
825        $match = false;
826        foreach ($attr as $thisvalue) {
827            if (preg_match($regex, $thisvalue, $matches_int)) {
828                $match = true;
829                $matches[] = $matches_int;
830            }
831        }
832
833        return $match;
834    }
835
836    /**
837     * Returns whether the entry is considered new (not present in the server).
838     *
839     * This method doesn't tell you if the entry is really not present on the
840     * server. Use {@link Horde_Ldap::exists()} to see if an entry is already
841     * there.
842     *
843     * @return boolean  True if this is considered a new entry.
844     */
845    public function isNew()
846    {
847        return $this->_new;
848    }
849
850    /**
851     * Is this entry going to be deleted once update() is called?
852     *
853     * @return boolean  True if this entry is going to be deleted.
854     */
855    public function willBeDeleted()
856    {
857        return $this->_delete;
858    }
859
860    /**
861     * Is this entry going to be moved once update() is called?
862     *
863     * @return boolean  True if this entry is going to be move.
864     */
865    public function willBeMoved()
866    {
867        return $this->dn() !== $this->currentDN();
868    }
869
870    /**
871     * Returns always the original DN.
872     *
873     * If an entry will be moved but {@link update()} was not called, {@link
874     * dn()} will return the new DN. This method however, returns always the
875     * current active DN.
876     *
877     * @return string  The current DN
878     */
879    public function currentDN()
880    {
881        return $this->_dn;
882    }
883
884    /**
885     * Returns the attribute changes to be carried out once update() is called.
886     *
887     * @return array  The due changes.
888     */
889    public function getChanges()
890    {
891        return $this->_changes;
892    }
893}
894