Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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 295178 2010-02-16 17:29:24Z 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.0.10');
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 = $this->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 = $this->errorMessage($error_code);
823
 
824
                if (($this->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 = $this->errorMessage($error_code);
919
 
920
                        if (($this->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 = $this->errorMessage($error_code);
958
 
959
                        if (($this->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($this->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($this->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 = $this->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 = $this->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
        // make dn relative to parent
1260
        $base = Net_LDAP2_Util::ldap_explode_dn($dn, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false));
1261
        if (self::isError($base)) {
1262
            return $base;
1263
        }
1264
        $entry_rdn = array_shift($base);
1265
        if (is_array($entry_rdn)) {
1266
            // maybe the dn consist of a multivalued RDN, we must build the dn in this case
1267
            // because the $entry_rdn is an array!
1268
            $filter_dn = Net_LDAP2_Util::canonical_dn($entry_rdn);
1269
        }
1270
        $base = Net_LDAP2_Util::canonical_dn($base);
1271
 
1272
        $result = @ldap_list($this->_link, $base, $entry_rdn, array(), 1, 1);
1273
        if (@ldap_count_entries($this->_link, $result)) {
1274
            return true;
1275
        }
1276
        if (ldap_errno($this->_link) == 32) {
1277
            return false;
1278
        }
1279
        if (ldap_errno($this->_link) != 0) {
1280
            return PEAR::raiseError(ldap_error($this->_link), ldap_errno($this->_link));
1281
        }
1282
        return false;
1283
    }
1284
 
1285
 
1286
    /**
1287
    * Get a specific entry based on the DN
1288
    *
1289
    * @param string $dn   DN of the entry that should be fetched
1290
    * @param array  $attr Array of Attributes to select. If ommitted, all attributes are fetched.
1291
    *
1292
    * @return Net_LDAP2_Entry|Net_LDAP2_Error    Reference to a Net_LDAP2_Entry object or Net_LDAP2_Error object
1293
    * @todo Maybe check against the shema should be done to be sure the attribute type exists
1294
    */
1295
    public function &getEntry($dn, $attr = array())
1296
    {
1297
        if (!is_array($attr)) {
1298
            $attr = array($attr);
1299
        }
1300
        $result = $this->search($dn, '(objectClass=*)',
1301
                                array('scope' => 'base', 'attributes' => $attr));
1302
        if (self::isError($result)) {
1303
            return $result;
1304
        } elseif ($result->count() == 0) {
1305
            return PEAR::raiseError('Could not fetch entry '.$dn.': no entry found');
1306
        }
1307
        $entry = $result->shiftEntry();
1308
        if (false == $entry) {
1309
            return PEAR::raiseError('Could not fetch entry (error retrieving entry from search result)');
1310
        }
1311
        return $entry;
1312
    }
1313
 
1314
    /**
1315
    * Rename or move an entry
1316
    *
1317
    * This method will instantly carry out an update() after the move,
1318
    * so the entry is moved instantly.
1319
    * You can pass an optional Net_LDAP2 object. In this case, a cross directory
1320
    * move will be performed which deletes the entry in the source (THIS) directory
1321
    * and adds it in the directory $target_ldap.
1322
    * A cross directory move will switch the Entrys internal LDAP reference so
1323
    * updates to the entry will go to the new directory.
1324
    *
1325
    * Note that if you want to do a cross directory move, you need to
1326
    * pass an Net_LDAP2_Entry object, otherwise the attributes will be empty.
1327
    *
1328
    * @param string|Net_LDAP2_Entry $entry       Entry DN or Entry object
1329
    * @param string                 $newdn       New location
1330
    * @param Net_LDAP2              $target_ldap (optional) Target directory for cross server move; should be passed via reference
1331
    *
1332
    * @return Net_LDAP2_Error|true
1333
    */
1334
    public function move($entry, $newdn, $target_ldap = null)
1335
    {
1336
        if (is_string($entry)) {
1337
            $entry_o = $this->getEntry($entry);
1338
        } else {
1339
            $entry_o =& $entry;
1340
        }
1341
        if (!$entry_o instanceof Net_LDAP2_Entry) {
1342
            return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object! (If DN was passed, conversion failed)');
1343
        }
1344
        if (null !== $target_ldap && !$target_ldap instanceof Net_LDAP2) {
1345
            return PEAR::raiseError('Parameter $target_ldap is expected to be a Net_LDAP2 object!');
1346
        }
1347
 
1348
        if ($target_ldap && $target_ldap !== $this) {
1349
            // cross directory move
1350
            if (is_string($entry)) {
1351
                return PEAR::raiseError('Unable to perform cross directory move: operation requires a Net_LDAP2_Entry object');
1352
            }
1353
            if ($target_ldap->dnExists($newdn)) {
1354
                return PEAR::raiseError('Unable to perform cross directory move: entry does exist in target directory');
1355
            }
1356
            $entry_o->dn($newdn);
1357
            $res = $target_ldap->add($entry_o);
1358
            if (self::isError($res)) {
1359
                return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in target directory');
1360
            }
1361
            $res = $this->delete($entry_o->currentDN());
1362
            if (self::isError($res)) {
1363
                $res2 = $target_ldap->delete($entry_o); // undo add
1364
                if (self::isError($res2)) {
1365
                    $add_error_string = 'Additionally, the deletion (undo add) of $entry in target directory failed.';
1366
                }
1367
                return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in source directory. '.$add_error_string);
1368
            }
1369
            $entry_o->setLDAP($target_ldap);
1370
            return true;
1371
        } else {
1372
            // local move
1373
            $entry_o->dn($newdn);
1374
            $entry_o->setLDAP($this);
1375
            return $entry_o->update();
1376
        }
1377
    }
1378
 
1379
    /**
1380
    * Copy an entry to a new location
1381
    *
1382
    * The entry will be immediately copied.
1383
    * Please note that only attributes you have
1384
    * selected will be copied.
1385
    *
1386
    * @param Net_LDAP2_Entry &$entry Entry object
1387
    * @param string          $newdn  New FQF-DN of the entry
1388
    *
1389
    * @return Net_LDAP2_Error|Net_LDAP2_Entry Error Message or reference to the copied entry
1390
    */
1391
    public function &copy(&$entry, $newdn)
1392
    {
1393
        if (!$entry instanceof Net_LDAP2_Entry) {
1394
            return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object!');
1395
        }
1396
 
1397
        $newentry = Net_LDAP2_Entry::createFresh($newdn, $entry->getValues());
1398
        $result   = $this->add($newentry);
1399
 
1400
        if ($result instanceof Net_LDAP2_Error) {
1401
            return $result;
1402
        } else {
1403
            return $newentry;
1404
        }
1405
    }
1406
 
1407
 
1408
    /**
1409
    * Returns the string for an ldap errorcode.
1410
    *
1411
    * Made to be able to make better errorhandling
1412
    * Function based on DB::errorMessage()
1413
    * Tip: The best description of the errorcodes is found here:
1414
    * http://www.directory-info.com/LDAP2/LDAPErrorCodes.html
1415
    *
1416
    * @param int $errorcode Error code
1417
    *
1418
    * @return string The errorstring for the error.
1419
    */
1420
    public function errorMessage($errorcode)
1421
    {
1422
        $errorMessages = array(
1423
                              0x00 => "LDAP_SUCCESS",
1424
                              0x01 => "LDAP_OPERATIONS_ERROR",
1425
                              0x02 => "LDAP_PROTOCOL_ERROR",
1426
                              0x03 => "LDAP_TIMELIMIT_EXCEEDED",
1427
                              0x04 => "LDAP_SIZELIMIT_EXCEEDED",
1428
                              0x05 => "LDAP_COMPARE_FALSE",
1429
                              0x06 => "LDAP_COMPARE_TRUE",
1430
                              0x07 => "LDAP_AUTH_METHOD_NOT_SUPPORTED",
1431
                              0x08 => "LDAP_STRONG_AUTH_REQUIRED",
1432
                              0x09 => "LDAP_PARTIAL_RESULTS",
1433
                              0x0a => "LDAP_REFERRAL",
1434
                              0x0b => "LDAP_ADMINLIMIT_EXCEEDED",
1435
                              0x0c => "LDAP_UNAVAILABLE_CRITICAL_EXTENSION",
1436
                              0x0d => "LDAP_CONFIDENTIALITY_REQUIRED",
1437
                              0x0e => "LDAP_SASL_BIND_INPROGRESS",
1438
                              0x10 => "LDAP_NO_SUCH_ATTRIBUTE",
1439
                              0x11 => "LDAP_UNDEFINED_TYPE",
1440
                              0x12 => "LDAP_INAPPROPRIATE_MATCHING",
1441
                              0x13 => "LDAP_CONSTRAINT_VIOLATION",
1442
                              0x14 => "LDAP_TYPE_OR_VALUE_EXISTS",
1443
                              0x15 => "LDAP_INVALID_SYNTAX",
1444
                              0x20 => "LDAP_NO_SUCH_OBJECT",
1445
                              0x21 => "LDAP_ALIAS_PROBLEM",
1446
                              0x22 => "LDAP_INVALID_DN_SYNTAX",
1447
                              0x23 => "LDAP_IS_LEAF",
1448
                              0x24 => "LDAP_ALIAS_DEREF_PROBLEM",
1449
                              0x30 => "LDAP_INAPPROPRIATE_AUTH",
1450
                              0x31 => "LDAP_INVALID_CREDENTIALS",
1451
                              0x32 => "LDAP_INSUFFICIENT_ACCESS",
1452
                              0x33 => "LDAP_BUSY",
1453
                              0x34 => "LDAP_UNAVAILABLE",
1454
                              0x35 => "LDAP_UNWILLING_TO_PERFORM",
1455
                              0x36 => "LDAP_LOOP_DETECT",
1456
                              0x3C => "LDAP_SORT_CONTROL_MISSING",
1457
                              0x3D => "LDAP_INDEX_RANGE_ERROR",
1458
                              0x40 => "LDAP_NAMING_VIOLATION",
1459
                              0x41 => "LDAP_OBJECT_CLASS_VIOLATION",
1460
                              0x42 => "LDAP_NOT_ALLOWED_ON_NONLEAF",
1461
                              0x43 => "LDAP_NOT_ALLOWED_ON_RDN",
1462
                              0x44 => "LDAP_ALREADY_EXISTS",
1463
                              0x45 => "LDAP_NO_OBJECT_CLASS_MODS",
1464
                              0x46 => "LDAP_RESULTS_TOO_LARGE",
1465
                              0x47 => "LDAP_AFFECTS_MULTIPLE_DSAS",
1466
                              0x50 => "LDAP_OTHER",
1467
                              0x51 => "LDAP_SERVER_DOWN",
1468
                              0x52 => "LDAP_LOCAL_ERROR",
1469
                              0x53 => "LDAP_ENCODING_ERROR",
1470
                              0x54 => "LDAP_DECODING_ERROR",
1471
                              0x55 => "LDAP_TIMEOUT",
1472
                              0x56 => "LDAP_AUTH_UNKNOWN",
1473
                              0x57 => "LDAP_FILTER_ERROR",
1474
                              0x58 => "LDAP_USER_CANCELLED",
1475
                              0x59 => "LDAP_PARAM_ERROR",
1476
                              0x5a => "LDAP_NO_MEMORY",
1477
                              0x5b => "LDAP_CONNECT_ERROR",
1478
                              0x5c => "LDAP_NOT_SUPPORTED",
1479
                              0x5d => "LDAP_CONTROL_NOT_FOUND",
1480
                              0x5e => "LDAP_NO_RESULTS_RETURNED",
1481
                              0x5f => "LDAP_MORE_RESULTS_TO_RETURN",
1482
                              0x60 => "LDAP_CLIENT_LOOP",
1483
                              0x61 => "LDAP_REFERRAL_LIMIT_EXCEEDED",
1484
                              1000 => "Unknown Net_LDAP2 Error"
1485
                              );
1486
 
1487
         return isset($errorMessages[$errorcode]) ?
1488
            $errorMessages[$errorcode] :
1489
            $errorMessages[NET_LDAP2_ERROR] . ' (' . $errorcode . ')';
1490
    }
1491
 
1492
    /**
1493
    * Gets a rootDSE object
1494
    *
1495
    * This either fetches a fresh rootDSE object or returns it from
1496
    * the internal cache for performance reasons, if possible.
1497
    *
1498
    * @param array $attrs Array of attributes to search for
1499
    *
1500
    * @access public
1501
    * @return Net_LDAP2_Error|Net_LDAP2_RootDSE Net_LDAP2_Error or Net_LDAP2_RootDSE object
1502
    */
1503
    public function &rootDse($attrs = null)
1504
    {
1505
        if ($attrs !== null && !is_array($attrs)) {
1506
            return PEAR::raiseError('Parameter $attr is expected to be an array!');
1507
        }
1508
 
1509
        $attrs_signature = serialize($attrs);
1510
 
1511
        // see if we need to fetch a fresh object, or if we already
1512
        // requested this object with the same attributes
1513
        if (true || !array_key_exists($attrs_signature, $this->_rootDSE_cache)) {
1514
            $rootdse =& Net_LDAP2_RootDSE::fetch($this, $attrs);
1515
            if ($rootdse instanceof Net_LDAP2_Error) {
1516
                return $rootdse;
1517
            }
1518
 
1519
            // search was ok, store rootDSE in cache
1520
            $this->_rootDSE_cache[$attrs_signature] = $rootdse;
1521
        }
1522
        return $this->_rootDSE_cache[$attrs_signature];
1523
    }
1524
 
1525
    /**
1526
    * Alias function of rootDse() for perl-ldap interface
1527
    *
1528
    * @access public
1529
    * @see rootDse()
1530
    * @return Net_LDAP2_Error|Net_LDAP2_RootDSE
1531
    */
1532
    public function &root_dse()
1533
    {
1534
        $args = func_get_args();
1535
        return call_user_func_array(array(&$this, 'rootDse'), $args);
1536
    }
1537
 
1538
    /**
1539
    * Get a schema object
1540
    *
1541
    * @param string $dn (optional) Subschema entry dn
1542
    *
1543
    * @access public
1544
    * @return Net_LDAP2_Schema|Net_LDAP2_Error  Net_LDAP2_Schema or Net_LDAP2_Error object
1545
    */
1546
    public function &schema($dn = null)
1547
    {
1548
        // Schema caching by Knut-Olav Hoven
1549
        // If a schema caching object is registered, we use that to fetch
1550
        // a schema object.
1551
        // See registerSchemaCache() for more info on this.
1552
        if ($this->_schema === null) {
1553
            if ($this->_schema_cache) {
1554
               $cached_schema = $this->_schema_cache->loadSchema();
1555
               if ($cached_schema instanceof Net_LDAP2_Error) {
1556
                   return $cached_schema; // route error to client
1557
               } else {
1558
                   if ($cached_schema instanceof Net_LDAP2_Schema) {
1559
                       $this->_schema = $cached_schema;
1560
                   }
1561
               }
1562
            }
1563
        }
1564
 
1565
        // Fetch schema, if not tried before and no cached version available.
1566
        // If we are already fetching the schema, we will skip fetching.
1567
        if ($this->_schema === null) {
1568
            // store a temporary error message so subsequent calls to schema() can
1569
            // detect, that we are fetching the schema already.
1570
            // Otherwise we will get an infinite loop at Net_LDAP2_Schema::fetch()
1571
            $this->_schema = new Net_LDAP2_Error('Schema not initialized');
1572
            $this->_schema = Net_LDAP2_Schema::fetch($this, $dn);
1573
 
1574
            // If schema caching is active, advise the cache to store the schema
1575
            if ($this->_schema_cache) {
1576
                $caching_result = $this->_schema_cache->storeSchema($this->_schema);
1577
                if ($caching_result instanceof Net_LDAP2_Error) {
1578
                    return $caching_result; // route error to client
1579
                }
1580
            }
1581
        }
1582
        return $this->_schema;
1583
    }
1584
 
1585
    /**
1586
    * Enable/disable persistent schema caching
1587
    *
1588
    * Sometimes it might be useful to allow your scripts to cache
1589
    * the schema information on disk, so the schema is not fetched
1590
    * every time the script runs which could make your scripts run
1591
    * faster.
1592
    *
1593
    * This method allows you to register a custom object that
1594
    * implements your schema cache. Please see the SchemaCache interface
1595
    * (SchemaCache.interface.php) for informations on how to implement this.
1596
    * To unregister the cache, pass null as $cache parameter.
1597
    *
1598
    * For ease of use, Net_LDAP2 provides a simple file based cache
1599
    * which is used in the example below. You may use this, for example,
1600
    * to store the schema in a linux tmpfs which results in the schema
1601
    * beeing cached inside the RAM which allows nearly instant access.
1602
    * <code>
1603
    *    // Create the simple file cache object that comes along with Net_LDAP2
1604
    *    $mySchemaCache_cfg = array(
1605
    *      'path'    =>  '/tmp/Net_LDAP2_Schema.cache',
1606
    *      'max_age' =>  86400   // max age is 24 hours (in seconds)
1607
    *    );
1608
    *    $mySchemaCache = new Net_LDAP2_SimpleFileSchemaCache($mySchemaCache_cfg);
1609
    *    $ldap = new Net_LDAP2::connect(...);
1610
    *    $ldap->registerSchemaCache($mySchemaCache); // enable caching
1611
    *    // now each call to $ldap->schema() will get the schema from disk!
1612
    * </code>
1613
    *
1614
    * @param Net_LDAP2_SchemaCache|null $cache Object implementing the Net_LDAP2_SchemaCache interface
1615
    *
1616
    * @return true|Net_LDAP2_Error
1617
    */
1618
    public function registerSchemaCache($cache) {
1619
        if (is_null($cache)
1620
        || (is_object($cache) && in_array('Net_LDAP2_SchemaCache', class_implements($cache))) ) {
1621
            $this->_schema_cache = $cache;
1622
            return true;
1623
        } else {
1624
            return new Net_LDAP2_Error('Custom schema caching object is either no '.
1625
                'valid object or does not implement the Net_LDAP2_SchemaCache interface!');
1626
        }
1627
    }
1628
 
1629
 
1630
    /**
1631
    * Checks if phps ldap-extension is loaded
1632
    *
1633
    * If it is not loaded, it tries to load it manually using PHPs dl().
1634
    * It knows both windows-dll and *nix-so.
1635
    *
1636
    * @static
1637
    * @return Net_LDAP2_Error|true
1638
    */
1639
    public static function checkLDAPExtension()
1640
    {
1641
        if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) {
1642
            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.");
1643
        } else {
1644
            return true;
1645
        }
1646
    }
1647
 
1648
    /**
1649
    * Encodes given attributes to UTF8 if needed by schema
1650
    *
1651
    * This function takes attributes in an array and then checks against the schema if they need
1652
    * UTF8 encoding. If that is so, they will be encoded. An encoded array will be returned and
1653
    * can be used for adding or modifying.
1654
    *
1655
    * $attributes is expected to be an array with keys describing
1656
    * the attribute names and the values as the value of this attribute:
1657
    * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1658
    *
1659
    * @param array $attributes Array of attributes
1660
    *
1661
    * @access public
1662
    * @return array|Net_LDAP2_Error Array of UTF8 encoded attributes or Error
1663
    */
1664
    public function utf8Encode($attributes)
1665
    {
1666
        return $this->utf8($attributes, 'utf8_encode');
1667
    }
1668
 
1669
    /**
1670
    * Decodes the given attribute values if needed by schema
1671
    *
1672
    * $attributes is expected to be an array with keys describing
1673
    * the attribute names and the values as the value of this attribute:
1674
    * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1675
    *
1676
    * @param array $attributes Array of attributes
1677
    *
1678
    * @access public
1679
    * @see utf8Encode()
1680
    * @return array|Net_LDAP2_Error Array with decoded attribute values or Error
1681
    */
1682
    public function utf8Decode($attributes)
1683
    {
1684
        return $this->utf8($attributes, 'utf8_decode');
1685
    }
1686
 
1687
    /**
1688
    * Encodes or decodes attribute values if needed
1689
    *
1690
    * @param array $attributes Array of attributes
1691
    * @param array $function   Function to apply to attribute values
1692
    *
1693
    * @access protected
1694
    * @return array|Net_LDAP2_Error Array of attributes with function applied to values or Error
1695
    */
1696
    protected function utf8($attributes, $function)
1697
    {
1698
        if (!is_array($attributes) || array_key_exists(0, $attributes)) {
1699
            return PEAR::raiseError('Parameter $attributes is expected to be an associative array');
1700
        }
1701
 
1702
        if (!$this->_schema) {
1703
            $this->_schema = $this->schema();
1704
        }
1705
 
1706
        if (!$this->_link || self::isError($this->_schema) || !function_exists($function)) {
1707
            return $attributes;
1708
        }
1709
 
1710
        if (is_array($attributes) && count($attributes) > 0) {
1711
 
1712
            foreach ($attributes as $k => $v) {
1713
 
1714
                if (!isset($this->_schemaAttrs[$k])) {
1715
 
1716
                    $attr = $this->_schema->get('attribute', $k);
1717
                    if (self::isError($attr)) {
1718
                        continue;
1719
                    }
1720
 
1721
                    if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) {
1722
                        $encode = true;
1723
                    } else {
1724
                        $encode = false;
1725
                    }
1726
                    $this->_schemaAttrs[$k] = $encode;
1727
 
1728
                } else {
1729
                    $encode = $this->_schemaAttrs[$k];
1730
                }
1731
 
1732
                if ($encode) {
1733
                    if (is_array($v)) {
1734
                        foreach ($v as $ak => $av) {
1735
                            $v[$ak] = call_user_func($function, $av);
1736
                        }
1737
                    } else {
1738
                        $v = call_user_func($function, $v);
1739
                    }
1740
                }
1741
                $attributes[$k] = $v;
1742
            }
1743
        }
1744
        return $attributes;
1745
    }
1746
 
1747
    /**
1748
    * Get the LDAP link resource.  It will loop attempting to
1749
    * re-establish the connection if the connection attempt fails and
1750
    * auto_reconnect has been turned on (see the _config array documentation).
1751
    *
1752
    * @access public
1753
    * @return resource LDAP link
1754
    */
1755
    public function &getLink()
1756
    {
1757
        if ($this->_config['auto_reconnect']) {
1758
            while (true) {
1759
                //
1760
                // Return the link handle if we are already connected.  Otherwise
1761
                // try to reconnect.
1762
                //
1763
                if ($this->_link !== false) {
1764
                    return $this->_link;
1765
                } else {
1766
                    $this->performReconnect();
1767
                }
1768
            }
1769
        }
1770
        return $this->_link;
1771
    }
1772
}
1773
 
1774
/**
1775
* Net_LDAP2_Error implements a class for reporting portable LDAP error messages.
1776
*
1777
* @category Net
1778
* @package  Net_LDAP2
1779
* @author   Tarjej Huse <tarjei@bergfald.no>
1780
* @license  http://www.gnu.org/copyleft/lesser.html LGPL
1781
* @link     http://pear.php.net/package/Net_LDAP22/
1782
*/
1783
class Net_LDAP2_Error extends PEAR_Error
1784
{
1785
    /**
1786
     * Net_LDAP2_Error constructor.
1787
     *
1788
     * @param string  $message   String with error message.
1789
     * @param integer $code      Net_LDAP2 error code
1790
     * @param integer $mode      what "error mode" to operate in
1791
     * @param mixed   $level     what error level to use for $mode & PEAR_ERROR_TRIGGER
1792
     * @param mixed   $debuginfo additional debug info, such as the last query
1793
     *
1794
     * @access public
1795
     * @see PEAR_Error
1796
     */
1797
    public function __construct($message = 'Net_LDAP2_Error', $code = NET_LDAP2_ERROR, $mode = PEAR_ERROR_RETURN,
1798
                         $level = E_USER_NOTICE, $debuginfo = null)
1799
    {
1800
        if (is_int($code)) {
1801
            $this->PEAR_Error($message . ': ' . Net_LDAP2::errorMessage($code), $code, $mode, $level, $debuginfo);
1802
        } else {
1803
            $this->PEAR_Error("$message: $code", NET_LDAP2_ERROR, $mode, $level, $debuginfo);
1804
        }
1805
    }
1806
}
1807
 
1808
?>