]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/LdapCommon/extlib/Net/LDAP2.php
Updated LDAP2 extlib to latest version.
[quix0rs-gnu-social.git] / plugins / LdapCommon / extlib / Net / LDAP2.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
3 /**
4 * File containing the Net_LDAP2 interface class.
5 *
6 * PHP version 5
7 *
8 * @category  Net
9 * @package   Net_LDAP2
10 * @author    Tarjej Huse <tarjei@bergfald.no>
11 * @author    Jan Wagner <wagner@netsols.de>
12 * @author    Del <del@babel.com.au>
13 * @author    Benedikt Hallinger <beni@php.net>
14 * @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger
15 * @license   http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
16 * @version   SVN: $Id: LDAP2.php 332308 2013-12-09 09:15:47Z beni $
17 * @link      http://pear.php.net/package/Net_LDAP2/
18 */
19
20 /**
21 * Package includes.
22 */
23 require_once 'PEAR.php';
24 require_once 'Net/LDAP2/RootDSE.php';
25 require_once 'Net/LDAP2/Schema.php';
26 require_once 'Net/LDAP2/Entry.php';
27 require_once 'Net/LDAP2/Search.php';
28 require_once 'Net/LDAP2/Util.php';
29 require_once 'Net/LDAP2/Filter.php';
30 require_once 'Net/LDAP2/LDIF.php';
31 require_once 'Net/LDAP2/SchemaCache.interface.php';
32 require_once 'Net/LDAP2/SimpleFileSchemaCache.php';
33
34 /**
35 *  Error constants for errors that are not LDAP errors.
36 */
37 define('NET_LDAP2_ERROR', 1000);
38
39 /**
40 * Net_LDAP2 Version
41 */
42 define('NET_LDAP2_VERSION', '2.1.0');
43
44 /**
45 * Net_LDAP2 - manipulate LDAP servers the right way!
46 *
47 * @category  Net
48 * @package   Net_LDAP2
49 * @author    Tarjej Huse <tarjei@bergfald.no>
50 * @author    Jan Wagner <wagner@netsols.de>
51 * @author    Del <del@babel.com.au>
52 * @author    Benedikt Hallinger <beni@php.net>
53 * @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger
54 * @license   http://www.gnu.org/copyleft/lesser.html LGPL
55 * @link      http://pear.php.net/package/Net_LDAP2/
56 */
57 class Net_LDAP2 extends PEAR
58 {
59     /**
60     * Class configuration array
61     *
62     * host     = the ldap host to connect to
63     *            (may be an array of several hosts to try)
64     * port     = the server port
65     * version  = ldap version (defaults to v 3)
66     * starttls = when set, ldap_start_tls() is run after connecting.
67     * bindpw   = no explanation needed
68     * binddn   = the DN to bind as.
69     * basedn   = ldap base
70     * options  = hash of ldap options to set (opt => val)
71     * filter   = default search filter
72     * scope    = default search scope
73     *
74     * Newly added in 2.0.0RC4, for auto-reconnect:
75     * auto_reconnect  = if set to true then the class will automatically
76     *                   attempt to reconnect to the LDAP server in certain
77     *                   failure conditionswhen attempting a search, or other
78     *                   LDAP operation.  Defaults to false.  Note that if you
79     *                   set this to true, calls to search() may block
80     *                   indefinitely if there is a catastrophic server failure.
81     * min_backoff     = minimum reconnection delay period (in seconds).
82     * current_backoff = initial reconnection delay period (in seconds).
83     * max_backoff     = maximum reconnection delay period (in seconds).
84     *
85     * @access protected
86     * @var array
87     */
88     protected $_config = array('host'            => 'localhost',
89                                'port'            => 389,
90                                'version'         => 3,
91                                'starttls'        => false,
92                                'binddn'          => '',
93                                'bindpw'          => '',
94                                'basedn'          => '',
95                                'options'         => array(),
96                                'filter'          => '(objectClass=*)',
97                                'scope'           => 'sub',
98                                'auto_reconnect'  => false,
99                                'min_backoff'     => 1,
100                                'current_backoff' => 1,
101                                'max_backoff'     => 32);
102
103     /**
104     * List of hosts we try to establish a connection to
105     *
106     * @access protected
107     * @var array
108     */
109     protected $_host_list = array();
110
111     /**
112     * List of hosts that are known to be down.
113     *
114     * @access protected
115     * @var array
116     */
117     protected $_down_host_list = array();
118
119     /**
120     * LDAP resource link.
121     *
122     * @access protected
123     * @var resource
124     */
125     protected $_link = false;
126
127     /**
128     * Net_LDAP2_Schema object
129     *
130     * This gets set and returned by {@link schema()}
131     *
132     * @access protected
133     * @var object Net_LDAP2_Schema
134     */
135     protected $_schema = null;
136
137     /**
138     * Schema cacher function callback
139     *
140     * @see registerSchemaCache()
141     * @var string
142     */
143     protected $_schema_cache = null;
144
145     /**
146     * Cache for attribute encoding checks
147     *
148     * @access protected
149     * @var array Hash with attribute names as key and boolean value
150     *            to determine whether they should be utf8 encoded or not.
151     */
152     protected $_schemaAttrs = array();
153
154     /**
155     * Cache for rootDSE objects
156     *
157     * Hash with requested rootDSE attr names as key and rootDSE object as value
158     *
159     * Since the RootDSE object itself may request a rootDSE object,
160     * {@link rootDse()} caches successful requests.
161     * Internally, Net_LDAP2 needs several lookups to this object, so
162     * caching increases performance significally.
163     *
164     * @access protected
165     * @var array
166     */
167     protected $_rootDSE_cache = array();
168
169     /**
170     * Returns the Net_LDAP2 Release version, may be called statically
171     *
172     * @static
173     * @return string Net_LDAP2 version
174     */
175     public static function getVersion()
176     {
177         return NET_LDAP2_VERSION;
178     }
179
180     /**
181     * Configure Net_LDAP2, connect and bind
182     *
183     * Use this method as starting point of using Net_LDAP2
184     * to establish a connection to your LDAP server.
185     *
186     * Static function that returns either an error object or the new Net_LDAP2
187     * object. Something like a factory. Takes a config array with the needed
188     * parameters.
189     *
190     * @param array $config Configuration array
191     *
192     * @access public
193     * @return Net_LDAP2_Error|Net_LDAP2   Net_LDAP2_Error or Net_LDAP2 object
194     */
195     public static function &connect($config = array())
196     {
197         $ldap_check = self::checkLDAPExtension();
198         if (self::iserror($ldap_check)) {
199             return $ldap_check;
200         }
201
202         @$obj = new Net_LDAP2($config);
203
204         // todo? better errorhandling for setConfig()?
205
206         // connect and bind with credentials in config
207         $err = $obj->bind();
208         if (self::isError($err)) {
209             return $err;
210         }
211
212         return $obj;
213     }
214
215     /**
216     * Net_LDAP2 constructor
217     *
218     * Sets the config array
219     *
220     * Please note that the usual way of getting Net_LDAP2 to work is
221     * to call something like:
222     * <code>$ldap = Net_LDAP2::connect($ldap_config);</code>
223     *
224     * @param array $config Configuration array
225     *
226     * @access protected
227     * @return void
228     * @see $_config
229     */
230     public function __construct($config = array())
231     {
232         $this->PEAR('Net_LDAP2_Error');
233         $this->setConfig($config);
234     }
235
236     /**
237     * Sets the internal configuration array
238     *
239     * @param array $config Configuration array
240     *
241     * @access protected
242     * @return void
243     */
244     protected function setConfig($config)
245     {
246         //
247         // Parameter check -- probably should raise an error here if config
248         // is not an array.
249         //
250         if (! is_array($config)) {
251             return;
252         }
253
254         foreach ($config as $k => $v) {
255             if (isset($this->_config[$k])) {
256                 $this->_config[$k] = $v;
257             } else {
258                 // map old (Net_LDAP2) parms to new ones
259                 switch($k) {
260                 case "dn":
261                     $this->_config["binddn"] = $v;
262                     break;
263                 case "password":
264                     $this->_config["bindpw"] = $v;
265                     break;
266                 case "tls":
267                     $this->_config["starttls"] = $v;
268                     break;
269                 case "base":
270                     $this->_config["basedn"] = $v;
271                     break;
272                 }
273             }
274         }
275
276         //
277         // Ensure the host list is an array.
278         //
279         if (is_array($this->_config['host'])) {
280             $this->_host_list = $this->_config['host'];
281         } else {
282             if (strlen($this->_config['host']) > 0) {
283                 $this->_host_list = array($this->_config['host']);
284             } else {
285                 $this->_host_list = array();
286                 // ^ this will cause an error in performConnect(),
287                 // so the user is notified about the failure
288             }
289         }
290
291         //
292         // Reset the down host list, which seems like a sensible thing to do
293         // if the config is being reset for some reason.
294         //
295         $this->_down_host_list = array();
296     }
297
298     /**
299     * Bind or rebind to the ldap-server
300     *
301     * This function binds with the given dn and password to the server. In case
302     * no connection has been made yet, it will be started and startTLS issued
303     * if appropiate.
304     *
305     * The internal bind configuration is not being updated, so if you call
306     * bind() without parameters, you can rebind with the credentials
307     * provided at first connecting to the server.
308     *
309     * @param string $dn       Distinguished name for binding
310     * @param string $password Password for binding
311     *
312     * @access public
313     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
314     */
315     public function bind($dn = null, $password = null)
316     {
317         // fetch current bind credentials
318         if (is_null($dn)) {
319             $dn = $this->_config["binddn"];
320         }
321         if (is_null($password)) {
322             $password = $this->_config["bindpw"];
323         }
324
325         // Connect first, if we haven't so far.
326         // This will also bind us to the server.
327         if ($this->_link === false) {
328             // store old credentials so we can revert them later
329             // then overwrite config with new bind credentials
330             $olddn = $this->_config["binddn"];
331             $oldpw = $this->_config["bindpw"];
332
333             // overwrite bind credentials in config
334             // so performConnect() knows about them
335             $this->_config["binddn"] = $dn;
336             $this->_config["bindpw"] = $password;
337
338             // try to connect with provided credentials
339             $msg = $this->performConnect();
340
341             // reset to previous config
342             $this->_config["binddn"] = $olddn;
343             $this->_config["bindpw"] = $oldpw;
344
345             // see if bind worked
346             if (self::isError($msg)) {
347                 return $msg;
348             }
349         } else {
350             // do the requested bind as we are
351             // asked to bind manually
352             if (is_null($dn)) {
353                 // anonymous bind
354                 $msg = @ldap_bind($this->_link);
355             } else {
356                 // privileged bind
357                 $msg = @ldap_bind($this->_link, $dn, $password);
358             }
359             if (false === $msg) {
360                 return PEAR::raiseError("Bind failed: " .
361                                         @ldap_error($this->_link),
362                                         @ldap_errno($this->_link));
363             }
364         }
365         return true;
366     }
367
368     /**
369     * Connect to the ldap-server
370     *
371     * This function connects to the LDAP server specified in
372     * the configuration, binds and set up the LDAP protocol as needed.
373     *
374     * @access protected
375     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
376     */
377     protected function performConnect()
378     {
379         // Note: Connecting is briefly described in RFC1777.
380         // Basicly it works like this:
381         //  1. set up TCP connection
382         //  2. secure that connection if neccessary
383         //  3a. setLDAPVersion to tell server which version we want to speak
384         //  3b. perform bind
385         //  3c. setLDAPVersion to tell server which version we want to speak
386         //      together with a test for supported versions
387         //  4. set additional protocol options
388
389         // Return true if we are already connected.
390         if ($this->_link !== false) {
391             return true;
392         }
393
394         // Connnect to the LDAP server if we are not connected.  Note that
395         // with some LDAP clients, ldapperformConnect returns a link value even
396         // if no connection is made.  We need to do at least one anonymous
397         // bind to ensure that a connection is actually valid.
398         //
399         // Ref: http://www.php.net/manual/en/function.ldap-connect.php
400
401         // Default error message in case all connection attempts
402         // fail but no message is set
403         $current_error = new PEAR_Error('Unknown connection error');
404
405         // Catch empty $_host_list arrays.
406         if (!is_array($this->_host_list) || count($this->_host_list) == 0) {
407             $current_error = PEAR::raiseError('No Servers configured! Please '.
408                'pass in an array of servers to Net_LDAP2');
409             return $current_error;
410         }
411
412         // Cycle through the host list.
413         foreach ($this->_host_list as $host) {
414
415             // Ensure we have a valid string for host name
416             if (is_array($host)) {
417                 $current_error = PEAR::raiseError('No Servers configured! '.
418                    'Please pass in an one dimensional array of servers to '.
419                    'Net_LDAP2! (multidimensional array detected!)');
420                 continue;
421             }
422
423             // Skip this host if it is known to be down.
424             if (in_array($host, $this->_down_host_list)) {
425                 continue;
426             }
427
428             // Record the host that we are actually connecting to in case
429             // we need it later.
430             $this->_config['host'] = $host;
431
432             // Attempt a connection.
433             $this->_link = @ldap_connect($host, $this->_config['port']);
434             if (false === $this->_link) {
435                 $current_error = PEAR::raiseError('Could not connect to ' .
436                     $host . ':' . $this->_config['port']);
437                 $this->_down_host_list[] = $host;
438                 continue;
439             }
440
441             // If we're supposed to use TLS, do so before we try to bind,
442             // as some strict servers only allow binding via secure connections
443             if ($this->_config["starttls"] === true) {
444                 if (self::isError($msg = $this->startTLS())) {
445                     $current_error           = $msg;
446                     $this->_link             = false;
447                     $this->_down_host_list[] = $host;
448                     continue;
449                 }
450             }
451
452             // Try to set the configured LDAP version on the connection if LDAP
453             // server needs that before binding (eg OpenLDAP).
454             // This could be necessary since rfc-1777 states that the protocol version
455             // has to be set at the bind request.
456             // We use force here which means that the test in the rootDSE is skipped;
457             // this is neccessary, because some strict LDAP servers only allow to
458             // read the LDAP rootDSE (which tells us the supported protocol versions)
459             // with authenticated clients.
460             // This may fail in which case we try again after binding.
461             // In this case, most probably the bind() or setLDAPVersion()-call
462             // below will also fail, providing error messages.
463             $version_set = false;
464             $ignored_err = $this->setLDAPVersion(0, true);
465             if (!self::isError($ignored_err)) {
466                 $version_set = true;
467             }
468
469             // Attempt to bind to the server. If we have credentials configured,
470             // we try to use them, otherwise its an anonymous bind.
471             // As stated by RFC-1777, the bind request should be the first
472             // operation to be performed after the connection is established.
473             // This may give an protocol error if the server does not support
474             // V2 binds and the above call to setLDAPVersion() failed.
475             // In case the above call failed, we try an V2 bind here and set the
476             // version afterwards (with checking to the rootDSE).
477             $msg = $this->bind();
478             if (self::isError($msg)) {
479                 // The bind failed, discard link and save error msg.
480                 // Then record the host as down and try next one
481                 if ($msg->getCode() == 0x02 && !$version_set) {
482                     // provide a finer grained error message
483                     // if protocol error arieses because of invalid version
484                     $msg = new Net_LDAP2_Error($msg->getMessage().
485                         " (could not set LDAP protocol version to ".
486                         $this->_config['version'].")",
487                         $msg->getCode());
488                 }
489                 $this->_link             = false;
490                 $current_error           = $msg;
491                 $this->_down_host_list[] = $host;
492                 continue;
493             }
494
495             // Set desired LDAP version if not successfully set before.
496             // Here, a check against the rootDSE is performed, so we get a
497             // error message if the server does not support the version.
498             // The rootDSE entry should tell us which LDAP versions are
499             // supported. However, some strict LDAP servers only allow
500             // bound suers to read the rootDSE.
501             if (!$version_set) {
502                 if (self::isError($msg = $this->setLDAPVersion())) {
503                     $current_error           = $msg;
504                     $this->_link             = false;
505                     $this->_down_host_list[] = $host;
506                     continue;
507                 }
508             }
509
510             // Set LDAP parameters, now we know we have a valid connection.
511             if (isset($this->_config['options']) &&
512                 is_array($this->_config['options']) &&
513                 count($this->_config['options'])) {
514                 foreach ($this->_config['options'] as $opt => $val) {
515                     $err = $this->setOption($opt, $val);
516                     if (self::isError($err)) {
517                         $current_error           = $err;
518                         $this->_link             = false;
519                         $this->_down_host_list[] = $host;
520                         continue 2;
521                     }
522                 }
523             }
524
525             // At this stage we have connected, bound, and set up options,
526             // so we have a known good LDAP server.  Time to go home.
527             return true;
528         }
529
530
531         // All connection attempts have failed, return the last error.
532         return $current_error;
533     }
534
535     /**
536     * Reconnect to the ldap-server.
537     *
538     * In case the connection to the LDAP
539     * service has dropped out for some reason, this function will reconnect,
540     * and re-bind if a bind has been attempted in the past.  It is probably
541     * most useful when the server list provided to the new() or connect()
542     * function is an array rather than a single host name, because in that
543     * case it will be able to connect to a failover or secondary server in
544     * case the primary server goes down.
545     *
546     * This doesn't return anything, it just tries to re-establish
547     * the current connection.  It will sleep for the current backoff
548     * period (seconds) before attempting the connect, and if the
549     * connection fails it will double the backoff period, but not
550     * try again.  If you want to ensure a reconnection during a
551     * transient period of server downtime then you need to call this
552     * function in a loop.
553     *
554     * @access protected
555     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
556     */
557     protected function performReconnect()
558     {
559
560         // Return true if we are already connected.
561         if ($this->_link !== false) {
562             return true;
563         }
564
565         // Default error message in case all connection attempts
566         // fail but no message is set
567         $current_error = new PEAR_Error('Unknown connection error');
568
569         // Sleep for a backoff period in seconds.
570         sleep($this->_config['current_backoff']);
571
572         // Retry all available connections.
573         $this->_down_host_list = array();
574         $msg = $this->performConnect();
575
576         // Bail out if that fails.
577         if (self::isError($msg)) {
578             $this->_config['current_backoff'] =
579                $this->_config['current_backoff'] * 2;
580             if ($this->_config['current_backoff'] > $this->_config['max_backoff']) {
581                 $this->_config['current_backoff'] = $this->_config['max_backoff'];
582             }
583             return $msg;
584         }
585
586         // Now we should be able to safely (re-)bind.
587         $msg = $this->bind();
588         if (self::isError($msg)) {
589             $this->_config['current_backoff'] = $this->_config['current_backoff'] * 2;
590             if ($this->_config['current_backoff'] > $this->_config['max_backoff']) {
591                 $this->_config['current_backoff'] = $this->_config['max_backoff'];
592             }
593
594             // _config['host'] should have had the last connected host stored in it
595             // by performConnect().  Since we are unable to bind to that host we can safely
596             // assume that it is down or has some other problem.
597             $this->_down_host_list[] = $this->_config['host'];
598             return $msg;
599         }
600
601         // At this stage we have connected, bound, and set up options,
602         // so we have a known good LDAP server. Time to go home.
603         $this->_config['current_backoff'] = $this->_config['min_backoff'];
604         return true;
605     }
606
607     /**
608     * Starts an encrypted session
609     *
610     * @access public
611     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
612     */
613     public function startTLS()
614     {
615         /* Test to see if the server supports TLS first.
616            This is done via testing the extensions offered by the server.
617            The OID 1.3.6.1.4.1.1466.20037 tells us, if TLS is supported.
618            Note, that not all servers allow to feth either the rootDSE or
619            attributes over an unencrypted channel, so we must ignore errors. */
620         $rootDSE = $this->rootDse();
621         if (self::isError($rootDSE)) {
622             /* IGNORE this error, because server may refuse fetching the
623                RootDSE over an unencrypted connection. */
624             //return $this->raiseError("Unable to fetch rootDSE entry ".
625             //"to see if TLS is supoported: ".$rootDSE->getMessage(), $rootDSE->getCode());
626         } else {
627             /* Fetch suceeded, see, if the server supports TLS. Again, we
628                ignore errors, because the server may refuse to return
629                attributes over unencryted connections. */
630             $supported_extensions = $rootDSE->getValue('supportedExtension');
631             if (self::isError($supported_extensions)) {
632                 /* IGNORE error, because server may refuse attribute
633                    returning over an unencrypted connection. */
634                 //return $this->raiseError("Unable to fetch rootDSE attribute 'supportedExtension' ".
635                 //"to see if TLS is supoported: ".$supported_extensions->getMessage(), $supported_extensions->getCode());
636             } else {
637                 // fetch succeedet, lets see if the server supports it.
638                 // if not, then drop an error. If supported, then do nothing,
639                 // because then we try to issue TLS afterwards.
640                 if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) {
641                     return $this->raiseError("Server reports that it does not support TLS.");
642                  }
643             }
644         }
645
646         // Try to establish TLS.
647         if (false === @ldap_start_tls($this->_link)) {
648             // Starting TLS failed. This may be an error, or because
649             // the server does not support it but did not enable us to
650             // detect that above.
651             return $this->raiseError("TLS could not be started: " .
652                                     @ldap_error($this->_link),
653                                     @ldap_errno($this->_link));
654         } else {
655             return true; // TLS is started now.
656         }
657     }
658
659     /**
660     * alias function of startTLS() for perl-ldap interface
661     *
662     * @return void
663     * @see startTLS()
664     */
665     public function start_tls()
666     {
667         $args = func_get_args();
668         return call_user_func_array(array( &$this, 'startTLS' ), $args);
669     }
670
671     /**
672     * Close LDAP connection.
673     *
674     * Closes the connection. Use this when the session is over.
675     *
676     * @return void
677     */
678     public function done()
679     {
680         $this->_Net_LDAP2();
681     }
682
683     /**
684     * Alias for {@link done()}
685     *
686     * @return void
687     * @see done()
688     */
689     public function disconnect()
690     {
691         $this->done();
692     }
693
694     /**
695     * Destructor
696     *
697     * @access protected
698     */
699     public function _Net_LDAP2()
700     {
701         @ldap_close($this->_link);
702     }
703
704     /**
705     * Add a new entryobject to a directory.
706     *
707     * Use add to add a new Net_LDAP2_Entry object to the directory.
708     * This also links the entry to the connection used for the add,
709     * if it was a fresh entry ({@link Net_LDAP2_Entry::createFresh()})
710     *
711     * @param Net_LDAP2_Entry &$entry Net_LDAP2_Entry
712     *
713     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
714     */
715     public function add(&$entry)
716     {
717         if (!$entry instanceof Net_LDAP2_Entry) {
718             return PEAR::raiseError('Parameter to Net_LDAP2::add() must be a Net_LDAP2_Entry object.');
719         }
720
721         // Continue attempting the add operation in a loop until we
722         // get a success, a definitive failure, or the world ends.
723         $foo = 0;
724         while (true) {
725             $link = $this->getLink();
726
727             if ($link === false) {
728                 // We do not have a successful connection yet.  The call to
729                 // getLink() would have kept trying if we wanted one.  Go
730                 // home now.
731                 return PEAR::raiseError("Could not add entry " . $entry->dn() .
732                        " no valid LDAP connection could be found.");
733             }
734
735             if (@ldap_add($link, $entry->dn(), $entry->getValues())) {
736                 // entry successfully added, we should update its $ldap reference
737                 // in case it is not set so far (fresh entry)
738                 if (!$entry->getLDAP() instanceof Net_LDAP2) {
739                     $entry->setLDAP($this);
740                 }
741                 // store, that the entry is present inside the directory
742                 $entry->markAsNew(false);
743                 return true;
744             } else {
745                 // We have a failure.  What type?  We may be able to reconnect
746                 // and try again.
747                 $error_code = @ldap_errno($link);
748                 $error_name = Net_LDAP2::errorMessage($error_code);
749
750                 if (($error_name === 'LDAP_OPERATIONS_ERROR') &&
751                     ($this->_config['auto_reconnect'])) {
752
753                     // The server has become disconnected before trying the
754                     // operation.  We should try again, possibly with a different
755                     // server.
756                     $this->_link = false;
757                     $this->performReconnect();
758                 } else {
759                     // Errors other than the above catched are just passed
760                     // back to the user so he may react upon them.
761                     return PEAR::raiseError("Could not add entry " . $entry->dn() . " " .
762                                             $error_name,
763                                             $error_code);
764                 }
765             }
766         }
767     }
768
769     /**
770     * Delete an entry from the directory
771     *
772     * The object may either be a string representing the dn or a Net_LDAP2_Entry
773     * object. When the boolean paramter recursive is set, all subentries of the
774     * entry will be deleted as well.
775     *
776     * @param string|Net_LDAP2_Entry $dn        DN-string or Net_LDAP2_Entry
777     * @param boolean                $recursive Should we delete all children recursive as well?
778     *
779     * @access public
780     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
781     */
782     public function delete($dn, $recursive = false)
783     {
784         if ($dn instanceof Net_LDAP2_Entry) {
785              $dn = $dn->dn();
786         }
787         if (false === is_string($dn)) {
788             return PEAR::raiseError("Parameter is not a string nor an entry object!");
789         }
790         // Recursive delete searches for children and calls delete for them
791         if ($recursive) {
792             $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0);
793             if (@ldap_count_entries($this->_link, $result)) {
794                 $subentry = @ldap_first_entry($this->_link, $result);
795                 $this->delete(@ldap_get_dn($this->_link, $subentry), true);
796                 while ($subentry = @ldap_next_entry($this->_link, $subentry)) {
797                     $this->delete(@ldap_get_dn($this->_link, $subentry), true);
798                 }
799             }
800         }
801
802         // Continue attempting the delete operation in a loop until we
803         // get a success, a definitive failure, or the world ends.
804         while (true) {
805             $link = $this->getLink();
806
807             if ($link === false) {
808                 // We do not have a successful connection yet.  The call to
809                 // getLink() would have kept trying if we wanted one.  Go
810                 // home now.
811                 return PEAR::raiseError("Could not add entry " . $dn .
812                        " no valid LDAP connection could be found.");
813             }
814
815             if (@ldap_delete($link, $dn)) {
816                 // entry successfully deleted.
817                 return true;
818             } else {
819                 // We have a failure.  What type?
820                 // We may be able to reconnect and try again.
821                 $error_code = @ldap_errno($link);
822                 $error_name = Net_LDAP2::errorMessage($error_code);
823
824                 if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') &&
825                     ($this->_config['auto_reconnect'])) {
826                     // The server has become disconnected before trying the
827                     // operation.  We should try again, possibly with a 
828                     // different server.
829                     $this->_link = false;
830                     $this->performReconnect();
831
832                 } elseif ($error_code == 66) {
833                     // Subentries present, server refused to delete.
834                     // Deleting subentries is the clients responsibility, but
835                     // since the user may not know of the subentries, we do not
836                     // force that here but instead notify the developer so he
837                     // may take actions himself.
838                     return PEAR::raiseError("Could not delete entry $dn because of subentries. Use the recursive parameter to delete them.");
839
840                 } else {
841                     // Errors other than the above catched are just passed
842                     // back to the user so he may react upon them.
843                     return PEAR::raiseError("Could not delete entry " . $dn . " " .
844                                             $error_name,
845                                             $error_code);
846                 }
847             }
848         }
849     }
850
851     /**
852     * Modify an ldapentry directly on the server
853     *
854     * This one takes the DN or a Net_LDAP2_Entry object and an array of actions.
855     * This array should be something like this:
856     *
857     * array('add' => array('attribute1' => array('val1', 'val2'),
858     *                      'attribute2' => array('val1')),
859     *       'delete' => array('attribute1'),
860     *       'replace' => array('attribute1' => array('val1')),
861     *       'changes' => array('add' => ...,
862     *                          'replace' => ...,
863     *                          'delete' => array('attribute1', 'attribute2' => array('val1')))
864     *
865     * The changes array is there so the order of operations can be influenced
866     * (the operations are done in order of appearance).
867     * The order of execution is as following:
868     *   1. adds from 'add' array
869     *   2. deletes from 'delete' array
870     *   3. replaces from 'replace' array
871     *   4. changes (add, replace, delete) in order of appearance
872     * All subarrays (add, replace, delete, changes) may be given at the same time.
873     *
874     * The function calls the corresponding functions of an Net_LDAP2_Entry
875     * object. A detailed description of array structures can be found there.
876     *
877     * Unlike the modification methods provided by the Net_LDAP2_Entry object,
878     * this method will instantly carry out an update() after each operation,
879     * thus modifying "directly" on the server.
880     *
881     * @param string|Net_LDAP2_Entry $entry DN-string or Net_LDAP2_Entry
882     * @param array                  $parms Array of changes
883     *
884     * @access public
885     * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true
886     */
887     public function modify($entry, $parms = array())
888     {
889         if (is_string($entry)) {
890             $entry = $this->getEntry($entry);
891             if (self::isError($entry)) {
892                 return $entry;
893             }
894         }
895         if (!$entry instanceof Net_LDAP2_Entry) {
896             return PEAR::raiseError("Parameter is not a string nor an entry object!");
897         }
898
899         // Perform changes mentioned separately
900         foreach (array('add', 'delete', 'replace') as $action) {
901             if (isset($parms[$action])) {
902                 $msg = $entry->$action($parms[$action]);
903                 if (self::isError($msg)) {
904                     return $msg;
905                 }
906                 $entry->setLDAP($this);
907
908                 // Because the @ldap functions are called inside Net_LDAP2_Entry::update(),
909                 // we have to trap the error codes issued from that if we want to support
910                 // reconnection.
911                 while (true) {
912                     $msg = $entry->update();
913
914                     if (self::isError($msg)) {
915                         // We have a failure.  What type?  We may be able to reconnect
916                         // and try again.
917                         $error_code = $msg->getCode();
918                         $error_name = Net_LDAP2::errorMessage($error_code);
919
920                         if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') &&
921                             ($this->_config['auto_reconnect'])) {
922
923                             // The server has become disconnected before trying the
924                             // operation.  We should try again, possibly with a different
925                             // server.
926                             $this->_link = false;
927                             $this->performReconnect();
928
929                         } else {
930
931                             // Errors other than the above catched are just passed
932                             // back to the user so he may react upon them.
933                             return PEAR::raiseError("Could not modify entry: ".$msg->getMessage());
934                         }
935                     } else {
936                         // modification succeedet, evaluate next change
937                         break;
938                     }
939                 }
940             }
941         }
942
943         // perform combined changes in 'changes' array
944         if (isset($parms['changes']) && is_array($parms['changes'])) {
945             foreach ($parms['changes'] as $action => $value) {
946
947                 // Because the @ldap functions are called inside Net_LDAP2_Entry::update,
948                 // we have to trap the error codes issued from that if we want to support
949                 // reconnection.
950                 while (true) {
951                     $msg = $this->modify($entry, array($action => $value));
952
953                     if (self::isError($msg)) {
954                         // We have a failure.  What type?  We may be able to reconnect
955                         // and try again.
956                         $error_code = $msg->getCode();
957                         $error_name = Net_LDAP2::errorMessage($error_code);
958
959                         if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') &&
960                             ($this->_config['auto_reconnect'])) {
961
962                             // The server has become disconnected before trying the
963                             // operation.  We should try again, possibly with a different
964                             // server.
965                             $this->_link = false;
966                             $this->performReconnect();
967
968                         } else {
969                             // Errors other than the above catched are just passed
970                             // back to the user so he may react upon them.
971                             return $msg;
972                         }
973                     } else {
974                         // modification succeedet, evaluate next change
975                         break;
976                     }
977                 }
978             }
979         }
980
981         return true;
982     }
983
984     /**
985     * Run a ldap search query
986     *
987     * Search is used to query the ldap-database.
988     * $base and $filter may be ommitted. The one from config will
989     * then be used. $base is either a DN-string or an Net_LDAP2_Entry
990     * object in which case its DN willb e used.
991     *
992     * Params may contain:
993     *
994     * scope: The scope which will be used for searching
995     *        base - Just one entry
996     *        sub  - The whole tree
997     *        one  - Immediately below $base
998     * sizelimit: Limit the number of entries returned (default: 0 = unlimited),
999     * timelimit: Limit the time spent for searching (default: 0 = unlimited),
1000     * attrsonly: If true, the search will only return the attribute names,
1001     * attributes: Array of attribute names, which the entry should contain.
1002     *             It is good practice to limit this to just the ones you need.
1003     * [NOT IMPLEMENTED]
1004     * deref: By default aliases are dereferenced to locate the base object for the search, but not when
1005     *        searching subordinates of the base object. This may be changed by specifying one of the
1006     *        following values:
1007     *
1008     *        never  - Do not dereference aliases in searching or in locating the base object of the search.
1009     *        search - Dereference aliases in subordinates of the base object in searching, but not in
1010     *                locating the base object of the search.
1011     *        find
1012     *        always
1013     *
1014     * Please note, that you cannot override server side limitations to sizelimit
1015     * and timelimit: You can always only lower a given limit.
1016     *
1017     * @param string|Net_LDAP2_Entry  $base   LDAP searchbase
1018     * @param string|Net_LDAP2_Filter $filter LDAP search filter or a Net_LDAP2_Filter object
1019     * @param array                   $params Array of options
1020     *
1021     * @access public
1022     * @return Net_LDAP2_Search|Net_LDAP2_Error Net_LDAP2_Search object or Net_LDAP2_Error object
1023     * @todo implement search controls (sorting etc)
1024     */
1025     public function search($base = null, $filter = null, $params = array())
1026     {
1027         if (is_null($base)) {
1028             $base = $this->_config['basedn'];
1029         }
1030         if ($base instanceof Net_LDAP2_Entry) {
1031             $base = $base->dn(); // fetch DN of entry, making searchbase relative to the entry
1032         }
1033         if (is_null($filter)) {
1034             $filter = $this->_config['filter'];
1035         }
1036         if ($filter instanceof Net_LDAP2_Filter) {
1037             $filter = $filter->asString(); // convert Net_LDAP2_Filter to string representation
1038         }
1039         if (PEAR::isError($filter)) {
1040             return $filter;
1041         }
1042         if (PEAR::isError($base)) {
1043             return $base;
1044         }
1045
1046         /* setting searchparameters  */
1047         (isset($params['sizelimit']))  ? $sizelimit  = $params['sizelimit']  : $sizelimit = 0;
1048         (isset($params['timelimit']))  ? $timelimit  = $params['timelimit']  : $timelimit = 0;
1049         (isset($params['attrsonly']))  ? $attrsonly  = $params['attrsonly']  : $attrsonly = 0;
1050         (isset($params['attributes'])) ? $attributes = $params['attributes'] : $attributes = array();
1051
1052         // Ensure $attributes to be an array in case only one
1053         // attribute name was given as string
1054         if (!is_array($attributes)) {
1055             $attributes = array($attributes);
1056         }
1057
1058         // reorganize the $attributes array index keys
1059         // sometimes there are problems with not consecutive indexes
1060         $attributes = array_values($attributes);
1061
1062         // scoping makes searches faster!
1063         $scope = (isset($params['scope']) ? $params['scope'] : $this->_config['scope']);
1064
1065         switch ($scope) {
1066         case 'one':
1067             $search_function = 'ldap_list';
1068             break;
1069         case 'base':
1070             $search_function = 'ldap_read';
1071             break;
1072         default:
1073             $search_function = 'ldap_search';
1074         }
1075
1076         // Continue attempting the search operation until we get a success
1077         // or a definitive failure.
1078         while (true) {
1079             $link = $this->getLink();
1080             $search = @call_user_func($search_function,
1081                                       $link,
1082                                       $base,
1083                                       $filter,
1084                                       $attributes,
1085                                       $attrsonly,
1086                                       $sizelimit,
1087                                       $timelimit);
1088
1089             if ($err = @ldap_errno($link)) {
1090                 if ($err == 32) {
1091                     // Errorcode 32 = no such object, i.e. a nullresult.
1092                     return $obj = new Net_LDAP2_Search ($search, $this, $attributes);
1093                 } elseif ($err == 4) {
1094                     // Errorcode 4 = sizelimit exeeded.
1095                     return $obj = new Net_LDAP2_Search ($search, $this, $attributes);
1096                 } elseif ($err == 87) {
1097                     // bad search filter
1098                     return $this->raiseError(Net_LDAP2::errorMessage($err) . "($filter)", $err);
1099                 } elseif (($err == 1) && ($this->_config['auto_reconnect'])) {
1100                     // Errorcode 1 = LDAP_OPERATIONS_ERROR but we can try a reconnect.
1101                     $this->_link = false;
1102                     $this->performReconnect();
1103                 } else {
1104                     $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope";
1105                     return $this->raiseError(Net_LDAP2::errorMessage($err) . $msg, $err);
1106                 }
1107             } else {
1108                 return $obj = new Net_LDAP2_Search($search, $this, $attributes);
1109             }
1110         }
1111     }
1112
1113     /**
1114     * Set an LDAP option
1115     *
1116     * @param string $option Option to set
1117     * @param mixed  $value  Value to set Option to
1118     *
1119     * @access public
1120     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
1121     */
1122     public function setOption($option, $value)
1123     {
1124         if ($this->_link) {
1125             if (defined($option)) {
1126                 if (@ldap_set_option($this->_link, constant($option), $value)) {
1127                     return true;
1128                 } else {
1129                     $err = @ldap_errno($this->_link);
1130                     if ($err) {
1131                         $msg = @ldap_err2str($err);
1132                     } else {
1133                         $err = NET_LDAP2_ERROR;
1134                         $msg = Net_LDAP2::errorMessage($err);
1135                     }
1136                     return $this->raiseError($msg, $err);
1137                 }
1138             } else {
1139                 return $this->raiseError("Unkown Option requested");
1140             }
1141         } else {
1142             return $this->raiseError("Could not set LDAP option: No LDAP connection");
1143         }
1144     }
1145
1146     /**
1147     * Get an LDAP option value
1148     *
1149     * @param string $option Option to get
1150     *
1151     * @access public
1152     * @return Net_LDAP2_Error|string Net_LDAP2_Error or option value
1153     */
1154     public function getOption($option)
1155     {
1156         if ($this->_link) {
1157             if (defined($option)) {
1158                 if (@ldap_get_option($this->_link, constant($option), $value)) {
1159                     return $value;
1160                 } else {
1161                     $err = @ldap_errno($this->_link);
1162                     if ($err) {
1163                         $msg = @ldap_err2str($err);
1164                     } else {
1165                         $err = NET_LDAP2_ERROR;
1166                         $msg = Net_LDAP2::errorMessage($err);
1167                     }
1168                     return $this->raiseError($msg, $err);
1169                 }
1170             } else {
1171                 $this->raiseError("Unkown Option requested");
1172             }
1173         } else {
1174             $this->raiseError("No LDAP connection");
1175         }
1176     }
1177
1178     /**
1179     * Get the LDAP_PROTOCOL_VERSION that is used on the connection.
1180     *
1181     * A lot of ldap functionality is defined by what protocol version the ldap server speaks.
1182     * This might be 2 or 3.
1183     *
1184     * @return int
1185     */
1186     public function getLDAPVersion()
1187     {
1188         if ($this->_link) {
1189             $version = $this->getOption("LDAP_OPT_PROTOCOL_VERSION");
1190         } else {
1191             $version = $this->_config['version'];
1192         }
1193         return $version;
1194     }
1195
1196     /**
1197     * Set the LDAP_PROTOCOL_VERSION that is used on the connection.
1198     *
1199     * @param int     $version LDAP-version that should be used
1200     * @param boolean $force   If set to true, the check against the rootDSE will be skipped
1201     *
1202     * @return Net_LDAP2_Error|true    Net_LDAP2_Error object or true
1203     * @todo Checking via the rootDSE takes much time - why? fetching and instanciation is quick!
1204     */
1205     public function setLDAPVersion($version = 0, $force = false)
1206     {
1207         if (!$version) {
1208             $version = $this->_config['version'];
1209         }
1210
1211         //
1212         // Check to see if the server supports this version first.
1213         //
1214         // Todo: Why is this so horribly slow?
1215         // $this->rootDse() is very fast, as well as Net_LDAP2_RootDSE::fetch()
1216         // seems like a problem at copiyng the object inside PHP??
1217         // Additionally, this is not always reproducable...
1218         //
1219         if (!$force) {
1220             $rootDSE = $this->rootDse();
1221             if ($rootDSE instanceof Net_LDAP2_Error) {
1222                 return $rootDSE;
1223             } else {
1224                 $supported_versions = $rootDSE->getValue('supportedLDAPVersion');
1225                 if (is_string($supported_versions)) {
1226                     $supported_versions = array($supported_versions);
1227                 }
1228                 $check_ok = in_array($version, $supported_versions);
1229             }
1230         }
1231
1232         if ($force || $check_ok) {
1233             return $this->setOption("LDAP_OPT_PROTOCOL_VERSION", $version);
1234         } else {
1235             return $this->raiseError("LDAP Server does not support protocol version " . $version);
1236         }
1237     }
1238
1239
1240     /**
1241     * Tells if a DN does exist in the directory
1242     *
1243     * @param string|Net_LDAP2_Entry $dn The DN of the object to test
1244     *
1245     * @return boolean|Net_LDAP2_Error
1246     */
1247     public function dnExists($dn)
1248     {
1249         if (PEAR::isError($dn)) {
1250             return $dn;
1251         }
1252         if ($dn instanceof Net_LDAP2_Entry) {
1253              $dn = $dn->dn();
1254         }
1255         if (false === is_string($dn)) {
1256             return PEAR::raiseError('Parameter $dn is not a string nor an entry object!');
1257         }
1258
1259         // search LDAP for that DN by performing a baselevel search for any
1260         // object. We can only find the DN in question this way, or nothing.
1261         $s_opts = array(
1262             'scope'      => 'base',
1263             'sizelimit'  => 1,
1264             'attributes' => '1.1' // select no attrs
1265         );
1266         $search = $this->search($dn, '(objectClass=*)', $s_opts);
1267
1268         if (self::isError($search)) {
1269             return $search;
1270         }
1271
1272         // retun wehter the DN exists; that is, we found an entry
1273         return ($search->count() == 0)? false : true;
1274     }
1275
1276
1277     /**
1278     * Get a specific entry based on the DN
1279     *
1280     * @param string $dn   DN of the entry that should be fetched
1281     * @param array  $attr Array of Attributes to select. If ommitted, all attributes are fetched.
1282     *
1283     * @return Net_LDAP2_Entry|Net_LDAP2_Error    Reference to a Net_LDAP2_Entry object or Net_LDAP2_Error object
1284     * @todo Maybe check against the shema should be done to be sure the attribute type exists
1285     */
1286     public function &getEntry($dn, $attr = array())
1287     {
1288         if (!is_array($attr)) {
1289             $attr = array($attr);
1290         }
1291         $result = $this->search($dn, '(objectClass=*)',
1292                                 array('scope' => 'base', 'attributes' => $attr));
1293         if (self::isError($result)) {
1294             return $result;
1295         } elseif ($result->count() == 0) {
1296             return PEAR::raiseError('Could not fetch entry '.$dn.': no entry found');
1297         }
1298         $entry = $result->shiftEntry();
1299         if (false == $entry) {
1300             return PEAR::raiseError('Could not fetch entry (error retrieving entry from search result)');
1301         }
1302         return $entry;
1303     }
1304
1305     /**
1306     * Rename or move an entry
1307     *
1308     * This method will instantly carry out an update() after the move,
1309     * so the entry is moved instantly.
1310     * You can pass an optional Net_LDAP2 object. In this case, a cross directory
1311     * move will be performed which deletes the entry in the source (THIS) directory
1312     * and adds it in the directory $target_ldap.
1313     * A cross directory move will switch the Entrys internal LDAP reference so
1314     * updates to the entry will go to the new directory.
1315     *
1316     * Note that if you want to do a cross directory move, you need to
1317     * pass an Net_LDAP2_Entry object, otherwise the attributes will be empty.
1318     *
1319     * @param string|Net_LDAP2_Entry $entry       Entry DN or Entry object
1320     * @param string                 $newdn       New location
1321     * @param Net_LDAP2              $target_ldap (optional) Target directory for cross server move; should be passed via reference
1322     *
1323     * @return Net_LDAP2_Error|true
1324     */
1325     public function move($entry, $newdn, $target_ldap = null)
1326     {
1327         if (is_string($entry)) {
1328             $entry_o = $this->getEntry($entry);
1329         } else {
1330             $entry_o =& $entry;
1331         }
1332         if (!$entry_o instanceof Net_LDAP2_Entry) {
1333             return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object! (If DN was passed, conversion failed)');
1334         }
1335         if (null !== $target_ldap && !$target_ldap instanceof Net_LDAP2) {
1336             return PEAR::raiseError('Parameter $target_ldap is expected to be a Net_LDAP2 object!');
1337         }
1338
1339         if ($target_ldap && $target_ldap !== $this) {
1340             // cross directory move
1341             if (is_string($entry)) {
1342                 return PEAR::raiseError('Unable to perform cross directory move: operation requires a Net_LDAP2_Entry object');
1343             }
1344             if ($target_ldap->dnExists($newdn)) {
1345                 return PEAR::raiseError('Unable to perform cross directory move: entry does exist in target directory');
1346             }
1347             $entry_o->dn($newdn);
1348             $res = $target_ldap->add($entry_o);
1349             if (self::isError($res)) {
1350                 return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in target directory');
1351             }
1352             $res = $this->delete($entry_o->currentDN());
1353             if (self::isError($res)) {
1354                 $res2 = $target_ldap->delete($entry_o); // undo add
1355                 if (self::isError($res2)) {
1356                     $add_error_string = 'Additionally, the deletion (undo add) of $entry in target directory failed.';
1357                 }
1358                 return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in source directory. '.$add_error_string);
1359             }
1360             $entry_o->setLDAP($target_ldap);
1361             return true;
1362         } else {
1363             // local move
1364             $entry_o->dn($newdn);
1365             $entry_o->setLDAP($this);
1366             return $entry_o->update();
1367         }
1368     }
1369
1370     /**
1371     * Copy an entry to a new location
1372     *
1373     * The entry will be immediately copied.
1374     * Please note that only attributes you have
1375     * selected will be copied.
1376     *
1377     * @param Net_LDAP2_Entry &$entry Entry object
1378     * @param string          $newdn  New FQF-DN of the entry
1379     *
1380     * @return Net_LDAP2_Error|Net_LDAP2_Entry Error Message or reference to the copied entry
1381     */
1382     public function &copy(&$entry, $newdn)
1383     {
1384         if (!$entry instanceof Net_LDAP2_Entry) {
1385             return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object!');
1386         }
1387
1388         $newentry = Net_LDAP2_Entry::createFresh($newdn, $entry->getValues());
1389         $result   = $this->add($newentry);
1390
1391         if ($result instanceof Net_LDAP2_Error) {
1392             return $result;
1393         } else {
1394             return $newentry;
1395         }
1396     }
1397
1398
1399     /**
1400     * Returns the string for an ldap errorcode.
1401     *
1402     * Made to be able to make better errorhandling
1403     * Function based on DB::errorMessage()
1404     * Tip: The best description of the errorcodes is found here:
1405     * http://www.directory-info.com/LDAP2/LDAPErrorCodes.html
1406     *
1407     * @param int $errorcode Error code
1408     *
1409     * @return string The errorstring for the error.
1410     */
1411     public static function errorMessage($errorcode)
1412     {
1413         $errorMessages = array(
1414                               0x00 => "LDAP_SUCCESS",
1415                               0x01 => "LDAP_OPERATIONS_ERROR",
1416                               0x02 => "LDAP_PROTOCOL_ERROR",
1417                               0x03 => "LDAP_TIMELIMIT_EXCEEDED",
1418                               0x04 => "LDAP_SIZELIMIT_EXCEEDED",
1419                               0x05 => "LDAP_COMPARE_FALSE",
1420                               0x06 => "LDAP_COMPARE_TRUE",
1421                               0x07 => "LDAP_AUTH_METHOD_NOT_SUPPORTED",
1422                               0x08 => "LDAP_STRONG_AUTH_REQUIRED",
1423                               0x09 => "LDAP_PARTIAL_RESULTS",
1424                               0x0a => "LDAP_REFERRAL",
1425                               0x0b => "LDAP_ADMINLIMIT_EXCEEDED",
1426                               0x0c => "LDAP_UNAVAILABLE_CRITICAL_EXTENSION",
1427                               0x0d => "LDAP_CONFIDENTIALITY_REQUIRED",
1428                               0x0e => "LDAP_SASL_BIND_INPROGRESS",
1429                               0x10 => "LDAP_NO_SUCH_ATTRIBUTE",
1430                               0x11 => "LDAP_UNDEFINED_TYPE",
1431                               0x12 => "LDAP_INAPPROPRIATE_MATCHING",
1432                               0x13 => "LDAP_CONSTRAINT_VIOLATION",
1433                               0x14 => "LDAP_TYPE_OR_VALUE_EXISTS",
1434                               0x15 => "LDAP_INVALID_SYNTAX",
1435                               0x20 => "LDAP_NO_SUCH_OBJECT",
1436                               0x21 => "LDAP_ALIAS_PROBLEM",
1437                               0x22 => "LDAP_INVALID_DN_SYNTAX",
1438                               0x23 => "LDAP_IS_LEAF",
1439                               0x24 => "LDAP_ALIAS_DEREF_PROBLEM",
1440                               0x30 => "LDAP_INAPPROPRIATE_AUTH",
1441                               0x31 => "LDAP_INVALID_CREDENTIALS",
1442                               0x32 => "LDAP_INSUFFICIENT_ACCESS",
1443                               0x33 => "LDAP_BUSY",
1444                               0x34 => "LDAP_UNAVAILABLE",
1445                               0x35 => "LDAP_UNWILLING_TO_PERFORM",
1446                               0x36 => "LDAP_LOOP_DETECT",
1447                               0x3C => "LDAP_SORT_CONTROL_MISSING",
1448                               0x3D => "LDAP_INDEX_RANGE_ERROR",
1449                               0x40 => "LDAP_NAMING_VIOLATION",
1450                               0x41 => "LDAP_OBJECT_CLASS_VIOLATION",
1451                               0x42 => "LDAP_NOT_ALLOWED_ON_NONLEAF",
1452                               0x43 => "LDAP_NOT_ALLOWED_ON_RDN",
1453                               0x44 => "LDAP_ALREADY_EXISTS",
1454                               0x45 => "LDAP_NO_OBJECT_CLASS_MODS",
1455                               0x46 => "LDAP_RESULTS_TOO_LARGE",
1456                               0x47 => "LDAP_AFFECTS_MULTIPLE_DSAS",
1457                               0x50 => "LDAP_OTHER",
1458                               0x51 => "LDAP_SERVER_DOWN",
1459                               0x52 => "LDAP_LOCAL_ERROR",
1460                               0x53 => "LDAP_ENCODING_ERROR",
1461                               0x54 => "LDAP_DECODING_ERROR",
1462                               0x55 => "LDAP_TIMEOUT",
1463                               0x56 => "LDAP_AUTH_UNKNOWN",
1464                               0x57 => "LDAP_FILTER_ERROR",
1465                               0x58 => "LDAP_USER_CANCELLED",
1466                               0x59 => "LDAP_PARAM_ERROR",
1467                               0x5a => "LDAP_NO_MEMORY",
1468                               0x5b => "LDAP_CONNECT_ERROR",
1469                               0x5c => "LDAP_NOT_SUPPORTED",
1470                               0x5d => "LDAP_CONTROL_NOT_FOUND",
1471                               0x5e => "LDAP_NO_RESULTS_RETURNED",
1472                               0x5f => "LDAP_MORE_RESULTS_TO_RETURN",
1473                               0x60 => "LDAP_CLIENT_LOOP",
1474                               0x61 => "LDAP_REFERRAL_LIMIT_EXCEEDED",
1475                               1000 => "Unknown Net_LDAP2 Error"
1476                               );
1477
1478          return isset($errorMessages[$errorcode]) ?
1479             $errorMessages[$errorcode] :
1480             $errorMessages[NET_LDAP2_ERROR] . ' (' . $errorcode . ')';
1481     }
1482
1483     /**
1484     * Gets a rootDSE object
1485     *
1486     * This either fetches a fresh rootDSE object or returns it from
1487     * the internal cache for performance reasons, if possible.
1488     *
1489     * @param array $attrs Array of attributes to search for
1490     *
1491     * @access public
1492     * @return Net_LDAP2_Error|Net_LDAP2_RootDSE Net_LDAP2_Error or Net_LDAP2_RootDSE object
1493     */
1494     public function &rootDse($attrs = null)
1495     {
1496         if ($attrs !== null && !is_array($attrs)) {
1497             return PEAR::raiseError('Parameter $attr is expected to be an array!');
1498         }
1499
1500         $attrs_signature = serialize($attrs);
1501
1502         // see if we need to fetch a fresh object, or if we already
1503         // requested this object with the same attributes
1504         if (true || !array_key_exists($attrs_signature, $this->_rootDSE_cache)) {
1505             $rootdse =& Net_LDAP2_RootDSE::fetch($this, $attrs);
1506             if ($rootdse instanceof Net_LDAP2_Error) {
1507                 return $rootdse;
1508             }
1509
1510             // search was ok, store rootDSE in cache
1511             $this->_rootDSE_cache[$attrs_signature] = $rootdse;
1512         }
1513         return $this->_rootDSE_cache[$attrs_signature];
1514     }
1515
1516     /**
1517     * Alias function of rootDse() for perl-ldap interface
1518     *
1519     * @access public
1520     * @see rootDse()
1521     * @return Net_LDAP2_Error|Net_LDAP2_RootDSE
1522     */
1523     public function &root_dse()
1524     {
1525         $args = func_get_args();
1526         return call_user_func_array(array(&$this, 'rootDse'), $args);
1527     }
1528
1529     /**
1530     * Get a schema object
1531     *
1532     * @param string $dn (optional) Subschema entry dn
1533     *
1534     * @access public
1535     * @return Net_LDAP2_Schema|Net_LDAP2_Error  Net_LDAP2_Schema or Net_LDAP2_Error object
1536     */
1537     public function &schema($dn = null)
1538     {
1539         // Schema caching by Knut-Olav Hoven
1540         // If a schema caching object is registered, we use that to fetch
1541         // a schema object.
1542         // See registerSchemaCache() for more info on this.
1543         if ($this->_schema === null) {
1544             if ($this->_schema_cache) {
1545                $cached_schema = $this->_schema_cache->loadSchema();
1546                if ($cached_schema instanceof Net_LDAP2_Error) {
1547                    return $cached_schema; // route error to client
1548                } else {
1549                    if ($cached_schema instanceof Net_LDAP2_Schema) {
1550                        $this->_schema = $cached_schema;
1551                    }
1552                }
1553             }
1554         }
1555
1556         // Fetch schema, if not tried before and no cached version available.
1557         // If we are already fetching the schema, we will skip fetching.
1558         if ($this->_schema === null) {
1559             // store a temporary error message so subsequent calls to schema() can
1560             // detect, that we are fetching the schema already.
1561             // Otherwise we will get an infinite loop at Net_LDAP2_Schema::fetch()
1562             $this->_schema = new Net_LDAP2_Error('Schema not initialized');
1563             $this->_schema = Net_LDAP2_Schema::fetch($this, $dn);
1564
1565             // If schema caching is active, advise the cache to store the schema
1566             if ($this->_schema_cache) {
1567                 $caching_result = $this->_schema_cache->storeSchema($this->_schema);
1568                 if ($caching_result instanceof Net_LDAP2_Error) {
1569                     return $caching_result; // route error to client
1570                 }
1571             }
1572         }
1573         return $this->_schema;
1574     }
1575
1576     /**
1577     * Enable/disable persistent schema caching
1578     *
1579     * Sometimes it might be useful to allow your scripts to cache
1580     * the schema information on disk, so the schema is not fetched
1581     * every time the script runs which could make your scripts run
1582     * faster.
1583     *
1584     * This method allows you to register a custom object that
1585     * implements your schema cache. Please see the SchemaCache interface
1586     * (SchemaCache.interface.php) for informations on how to implement this.
1587     * To unregister the cache, pass null as $cache parameter.
1588     *
1589     * For ease of use, Net_LDAP2 provides a simple file based cache
1590     * which is used in the example below. You may use this, for example,
1591     * to store the schema in a linux tmpfs which results in the schema
1592     * beeing cached inside the RAM which allows nearly instant access.
1593     * <code>
1594     *    // Create the simple file cache object that comes along with Net_LDAP2
1595     *    $mySchemaCache_cfg = array(
1596     *      'path'    =>  '/tmp/Net_LDAP2_Schema.cache',
1597     *      'max_age' =>  86400   // max age is 24 hours (in seconds)
1598     *    );
1599     *    $mySchemaCache = new Net_LDAP2_SimpleFileSchemaCache($mySchemaCache_cfg);
1600     *    $ldap = new Net_LDAP2::connect(...);
1601     *    $ldap->registerSchemaCache($mySchemaCache); // enable caching
1602     *    // now each call to $ldap->schema() will get the schema from disk!
1603     * </code>
1604     *
1605     * @param Net_LDAP2_SchemaCache|null $cache Object implementing the Net_LDAP2_SchemaCache interface
1606     *
1607     * @return true|Net_LDAP2_Error
1608     */
1609     public function registerSchemaCache($cache) {
1610         if (is_null($cache)
1611         || (is_object($cache) && in_array('Net_LDAP2_SchemaCache', class_implements($cache))) ) {
1612             $this->_schema_cache = $cache;
1613             return true;
1614         } else {
1615             return new Net_LDAP2_Error('Custom schema caching object is either no '.
1616                 'valid object or does not implement the Net_LDAP2_SchemaCache interface!');
1617         }
1618     }
1619
1620
1621     /**
1622     * Checks if phps ldap-extension is loaded
1623     *
1624     * If it is not loaded, it tries to load it manually using PHPs dl().
1625     * It knows both windows-dll and *nix-so.
1626     *
1627     * @static
1628     * @return Net_LDAP2_Error|true
1629     */
1630     public static function checkLDAPExtension()
1631     {
1632         if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) {
1633             return new Net_LDAP2_Error("It seems that you do not have the ldap-extension installed. Please install it before using the Net_LDAP2 package.");
1634         } else {
1635             return true;
1636         }
1637     }
1638
1639     /**
1640     * Encodes given attributes from ISO-8859-1 to UTF-8 if needed by schema
1641     *
1642     * This function takes attributes in an array and then checks against the schema if they need
1643     * UTF8 encoding. If that is so, they will be encoded. An encoded array will be returned and
1644     * can be used for adding or modifying.
1645     *
1646     * $attributes is expected to be an array with keys describing
1647     * the attribute names and the values as the value of this attribute:
1648     * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1649     *
1650     * @param array $attributes Array of attributes
1651     *
1652     * @access public
1653     * @return array|Net_LDAP2_Error Array of UTF8 encoded attributes or Error
1654     */
1655     public function utf8Encode($attributes)
1656     {
1657         return $this->utf8($attributes, 'utf8_encode');
1658     }
1659
1660     /**
1661     * Decodes the given attribute values from UTF-8 to ISO-8859-1 if needed by schema
1662     *
1663     * $attributes is expected to be an array with keys describing
1664     * the attribute names and the values as the value of this attribute:
1665     * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1666     *
1667     * @param array $attributes Array of attributes
1668     *
1669     * @access public
1670     * @see utf8Encode()
1671     * @return array|Net_LDAP2_Error Array with decoded attribute values or Error
1672     */
1673     public function utf8Decode($attributes)
1674     {
1675         return $this->utf8($attributes, 'utf8_decode');
1676     }
1677
1678     /**
1679     * Encodes or decodes UTF-8/ISO-8859-1 attribute values if needed by schema
1680     *
1681     * @param array $attributes Array of attributes
1682     * @param array $function   Function to apply to attribute values
1683     *
1684     * @access protected
1685     * @return array|Net_LDAP2_Error Array of attributes with function applied to values or Error
1686     */
1687     protected function utf8($attributes, $function)
1688     {
1689         if (!is_array($attributes) || array_key_exists(0, $attributes)) {
1690             return PEAR::raiseError('Parameter $attributes is expected to be an associative array');
1691         }
1692
1693         if (!$this->_schema) {
1694             $this->_schema = $this->schema();
1695         }
1696
1697         if (!$this->_link || self::isError($this->_schema) || !function_exists($function)) {
1698             return $attributes;
1699         }
1700
1701         if (is_array($attributes) && count($attributes) > 0) {
1702
1703             foreach ($attributes as $k => $v) {
1704
1705                 if (!isset($this->_schemaAttrs[$k])) {
1706
1707                     $attr = $this->_schema->get('attribute', $k);
1708                     if (self::isError($attr)) {
1709                         continue;
1710                     }
1711
1712                     // Encoding is needed if this is a DIR_STR. We assume also
1713                     // needed encoding in case the schema contains no syntax
1714                     // information (he does not need to, see rfc2252, 4.2)
1715                     if (!array_key_exists('syntax', $attr) || false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) {
1716                         $encode = true;
1717                     } else {
1718                         $encode = false;
1719                     }
1720                     $this->_schemaAttrs[$k] = $encode;
1721
1722                 } else {
1723                     $encode = $this->_schemaAttrs[$k];
1724                 }
1725
1726                 if ($encode) {
1727                     if (is_array($v)) {
1728                         foreach ($v as $ak => $av) {
1729                             $v[$ak] = call_user_func($function, $av);
1730                         }
1731                     } else {
1732                         $v = call_user_func($function, $v);
1733                     }
1734                 }
1735                 $attributes[$k] = $v;
1736             }
1737         }
1738         return $attributes;
1739     }
1740
1741     /**
1742     * Get the LDAP link resource.  It will loop attempting to
1743     * re-establish the connection if the connection attempt fails and
1744     * auto_reconnect has been turned on (see the _config array documentation).
1745     *
1746     * @access public
1747     * @return resource LDAP link
1748     */
1749     public function &getLink()
1750     {
1751         if ($this->_config['auto_reconnect']) {
1752             while (true) {
1753                 //
1754                 // Return the link handle if we are already connected.  Otherwise
1755                 // try to reconnect.
1756                 //
1757                 if ($this->_link !== false) {
1758                     return $this->_link;
1759                 } else {
1760                     $this->performReconnect();
1761                 }
1762             }
1763         }
1764         return $this->_link;
1765     }
1766 }
1767
1768 /**
1769 * Net_LDAP2_Error implements a class for reporting portable LDAP error messages.
1770 *
1771 * @category Net
1772 * @package  Net_LDAP2
1773 * @author   Tarjej Huse <tarjei@bergfald.no>
1774 * @license  http://www.gnu.org/copyleft/lesser.html LGPL
1775 * @link     http://pear.php.net/package/Net_LDAP22/
1776 */
1777 class Net_LDAP2_Error extends PEAR_Error
1778 {
1779     /**
1780      * Net_LDAP2_Error constructor.
1781      *
1782      * @param string  $message   String with error message.
1783      * @param integer $code      Net_LDAP2 error code
1784      * @param integer $mode      what "error mode" to operate in
1785      * @param mixed   $level     what error level to use for $mode & PEAR_ERROR_TRIGGER
1786      * @param mixed   $debuginfo additional debug info, such as the last query
1787      *
1788      * @access public
1789      * @see PEAR_Error
1790      */
1791     public function __construct($message = 'Net_LDAP2_Error', $code = NET_LDAP2_ERROR, $mode = PEAR_ERROR_RETURN,
1792                          $level = E_USER_NOTICE, $debuginfo = null)
1793     {
1794         if (is_int($code)) {
1795             $this->PEAR_Error($message . ': ' . Net_LDAP2::errorMessage($code), $code, $mode, $level, $debuginfo);
1796         } else {
1797             $this->PEAR_Error("$message: $code", NET_LDAP2_ERROR, $mode, $level, $debuginfo);
1798         }
1799     }
1800 }
1801
1802 ?>