Overview

Packages

  • Ldap

Classes

  • Horde_Ldap
  • Horde_Ldap_Entry
  • Horde_Ldap_Exception
  • Horde_Ldap_Filter
  • Horde_Ldap_Ldif
  • Horde_Ldap_RootDse
  • Horde_Ldap_Schema
  • Horde_Ldap_Search
  • Horde_Ldap_Util
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * The main Horde_Ldap class.
   4:  *
   5:  * Copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger
   6:  * Copyright 2009-2012 Horde LLC (http://www.horde.org/)
   7:  *
   8:  * @package   Ldap
   9:  * @author    Tarjej Huse <tarjei@bergfald.no>
  10:  * @author    Jan Wagner <wagner@netsols.de>
  11:  * @author    Del <del@babel.com.au>
  12:  * @author    Benedikt Hallinger <beni@php.net>
  13:  * @author    Ben Klang <ben@alkaloid.net>
  14:  * @author    Chuck Hagenbuch <chuck@horde.org>
  15:  * @author    Jan Schneider <jan@horde.org>
  16:  * @license   http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
  17:  */
  18: class Horde_Ldap
  19: {
  20:     /**
  21:      * Class configuration array
  22:      *
  23:      * - hostspec:       the LDAP host to connect to (may be an array of
  24:      *                   several hosts to try).
  25:      * - port:           the server port.
  26:      * - version:        LDAP version (defaults to 3).
  27:      * - tls:            when set, ldap_start_tls() is run after connecting.
  28:      * - binddn:         the DN to bind as when searching.
  29:      * - bindpw:         password to use when searching LDAP.
  30:      * - basedn:         LDAP base.
  31:      * - options:        hash of LDAP options to set.
  32:      * - filter:         default search filter.
  33:      * - scope:          default search scope.
  34:      * - user:           configuration parameters for {@link findUserDN()},
  35:      *                   must contain 'uid', and 'filter' or 'objectclass'
  36:      *                   entries.
  37:      * - auto_reconnect: if true, the class will automatically
  38:      *                   attempt to reconnect to the LDAP server in certain
  39:      *                   failure conditions when attempting a search, or other
  40:      *                   LDAP operations.  Defaults to false.  Note that if you
  41:      *                   set this to true, calls to search() may block
  42:      *                   indefinitely if there is a catastrophic server failure.
  43:      * - min_backoff:    minimum reconnection delay period (in seconds).
  44:      * - current_backof: initial reconnection delay period (in seconds).
  45:      * - max_backoff:    maximum reconnection delay period (in seconds).
  46:      * - cache           a Horde_Cache instance for caching schema requests.
  47:      *
  48:      * @var array
  49:      */
  50:     protected $_config = array(
  51:         'hostspec'        => 'localhost',
  52:         'port'            => 389,
  53:         'version'         => 3,
  54:         'tls'             => false,
  55:         'binddn'          => '',
  56:         'bindpw'          => '',
  57:         'basedn'          => '',
  58:         'options'         => array(),
  59:         'filter'          => '(objectClass=*)',
  60:         'scope'           => 'sub',
  61:         'user'            => array(),
  62:         'auto_reconnect'  => false,
  63:         'min_backoff'     => 1,
  64:         'current_backoff' => 1,
  65:         'max_backoff'     => 32,
  66:         'cache'           => false,
  67:         'cachettl'        => 3600);
  68: 
  69:     /**
  70:      * List of hosts we try to establish a connection to.
  71:      *
  72:      * @var array
  73:      */
  74:     protected $_hostList = array();
  75: 
  76:     /**
  77:      * List of hosts that are known to be down.
  78:      *
  79:      * @var array
  80:      */
  81:     protected $_downHostList = array();
  82: 
  83:     /**
  84:      * LDAP resource link.
  85:      *
  86:      * @var resource
  87:      */
  88:     protected $_link;
  89: 
  90:     /**
  91:      * Schema object.
  92:      *
  93:      * @see schema()
  94:      * @var Horde_Ldap_Schema
  95:      */
  96:     protected $_schema;
  97: 
  98:     /**
  99:      * Schema cache function callback.
 100:      *
 101:      * @see registerSchemaCache()
 102:      * @var string
 103:      */
 104:     protected $_schemaCache;
 105: 
 106:     /**
 107:      * Cache for attribute encoding checks.
 108:      *
 109:      * @var array Hash with attribute names as key and boolean value
 110:      *            to determine whether they should be utf8 encoded or not.
 111:      */
 112:     protected $_schemaAttrs = array();
 113: 
 114:     /**
 115:      * Cache for rootDSE objects
 116:      *
 117:      * Hash with requested rootDSE attr names as key and rootDSE
 118:      * object as value.
 119:      *
 120:      * Since the RootDSE object itself may request a rootDSE object,
 121:      * {@link rootDSE()} caches successful requests.
 122:      * Internally, Horde_Ldap needs several lookups to this object, so
 123:      * caching increases performance significally.
 124:      *
 125:      * @var array
 126:      */
 127:     protected $_rootDSECache = array();
 128: 
 129:     /**
 130:      * Constructor.
 131:      *
 132:      * @see $_config
 133:      *
 134:      * @param array $config Configuration array.
 135:      */
 136:     public function __construct($config = array())
 137:     {
 138:         if (!Horde_Util::loadExtension('ldap')) {
 139:             throw new Horde_Ldap_Exception('No PHP LDAP extension');
 140:         }
 141:         $this->setConfig($config);
 142:         $this->bind();
 143:     }
 144: 
 145:     /**
 146:      * Destructor.
 147:      */
 148:     public function __destruct()
 149:     {
 150:         $this->disconnect();
 151:     }
 152: 
 153:     /**
 154:      * Sets the internal configuration array.
 155:      *
 156:      * @param array $config Configuration hash.
 157:      */
 158:     protected function setConfig($config)
 159:     {
 160:         /* Parameter check -- probably should raise an error here if
 161:          * config is not an array. */
 162:         if (!is_array($config)) {
 163:             return;
 164:         }
 165: 
 166:         foreach ($config as $k => $v) {
 167:             if (isset($this->_config[$k])) {
 168:                 $this->_config[$k] = $v;
 169:             }
 170:         }
 171: 
 172:         /* Ensure the host list is an array. */
 173:         if (is_array($this->_config['hostspec'])) {
 174:             $this->_hostList = $this->_config['hostspec'];
 175:         } else {
 176:             if (strlen($this->_config['hostspec'])) {
 177:                 $this->_hostList = array($this->_config['hostspec']);
 178:             } else {
 179:                 $this->_hostList = array();
 180:                 /* This will cause an error in _connect(), so
 181:                  * the user is notified about the failure. */
 182:             }
 183:         }
 184: 
 185:         /* Reset the down host list, which seems like a sensible thing
 186:          * to do if the config is being reset for some reason. */
 187:         $this->_downHostList = array();
 188:     }
 189: 
 190:     /**
 191:      * Bind or rebind to the LDAP server.
 192:      *
 193:      * This function binds with the given DN and password to the
 194:      * server. In case no connection has been made yet, it will be
 195:      * started and STARTTLS issued if appropiate.
 196:      *
 197:      * The internal bind configuration is not being updated, so if you
 198:      * call bind() without parameters, you can rebind with the
 199:      * credentials provided at first connecting to the server.
 200:      *
 201:      * @param string $dn       DN for binding.
 202:      * @param string $password Password for binding.
 203:      *
 204:      * @throws Horde_Ldap_Exception
 205:      */
 206:     public function bind($dn = null, $password = null)
 207:     {
 208:         /* Fetch current bind credentials. */
 209:         if (empty($dn)) {
 210:             $dn = $this->_config['binddn'];
 211:         }
 212:         if (empty($password)) {
 213:             $password = $this->_config['bindpw'];
 214:         }
 215: 
 216:         /* Connect first, if we haven't so far.  This will also bind
 217:          * us to the server. */
 218:         if (!$this->_link) {
 219:             /* Store old credentials so we can revert them later, then
 220:              * overwrite config with new bind credentials. */
 221:             $olddn = $this->_config['binddn'];
 222:             $oldpw = $this->_config['bindpw'];
 223: 
 224:             /* Overwrite bind credentials in config so
 225:              * _connect() knows about them. */
 226:             $this->_config['binddn'] = $dn;
 227:             $this->_config['bindpw'] = $password;
 228: 
 229:             /* Try to connect with provided credentials. */
 230:             $msg = $this->_connect();
 231: 
 232:             /* Reset to previous config. */
 233:             $this->_config['binddn'] = $olddn;
 234:             $this->_config['bindpw'] = $oldpw;
 235:             return;
 236:         }
 237: 
 238:         /* Do the requested bind as we are asked to bind manually. */
 239:         if (empty($dn)) {
 240:             /* Anonymous bind. */
 241:             $msg = @ldap_bind($this->_link);
 242:         } else {
 243:             /* Privileged bind. */
 244:             $msg = @ldap_bind($this->_link, $dn, $password);
 245:         }
 246:         if (!$msg) {
 247:             throw new Horde_Ldap_Exception('Bind failed: ' . @ldap_error($this->_link),
 248:                                            @ldap_errno($this->_link));
 249:         }
 250:     }
 251: 
 252:     /**
 253:      * Connects to the LDAP server.
 254:      *
 255:      * This function connects to the LDAP server specified in the
 256:      * configuration, binds and set up the LDAP protocol as needed.
 257:      *
 258:      * @throws Horde_Ldap_Exception
 259:      */
 260:     protected function _connect()
 261:     {
 262:         /* Connecting is briefly described in RFC1777. Basicly it works like
 263:          * this:
 264:          *  1. set up TCP connection
 265:          *  2. secure that connection if neccessary
 266:          *  3a. setVersion to tell server which version we want to speak
 267:          *  3b. perform bind
 268:          *  3c. setVersion to tell server which version we want to speak
 269:          *      together with a test for supported versions
 270:          *  4. set additional protocol options */
 271: 
 272:         /* Return if we are already connected. */
 273:         if ($this->_link) {
 274:             return;
 275:         }
 276: 
 277:         /* Connnect to the LDAP server if we are not connected.  Note that
 278:          * ldap_connect() may return a link value even if no connection is
 279:          * made.  We need to do at least one anonymous bind to ensure that a
 280:          * connection is actually valid.
 281:          *
 282:          * See: http://www.php.net/manual/en/function.ldap-connect.php */
 283: 
 284:         /* Default error message in case all connection attempts fail but no
 285:          * message is set. */
 286:         $current_error = new Horde_Ldap_Exception('Unknown connection error');
 287: 
 288:         /* Catch empty $_hostList arrays. */
 289:         if (!is_array($this->_hostList) || !count($this->_hostList)) {
 290:             throw new Horde_Ldap_Exception('No servers configured');
 291:         }
 292: 
 293:         /* Cycle through the host list. */
 294:         foreach ($this->_hostList as $host) {
 295:             /* Ensure we have a valid string for host name. */
 296:             if (is_array($host)) {
 297:                 $current_error = new Horde_Ldap_Exception('No Servers configured');
 298:                 continue;
 299:             }
 300: 
 301:             /* Skip this host if it is known to be down. */
 302:             if (in_array($host, $this->_downHostList)) {
 303:                 continue;
 304:             }
 305: 
 306:             /* Record the host that we are actually connecting to in case we
 307:              * need it later. */
 308:             $this->_config['hostspec'] = $host;
 309: 
 310:             /* Attempt a connection. */
 311:             $this->_link = @ldap_connect($host, $this->_config['port']);
 312:             if (!$this->_link) {
 313:                 $current_error = new Horde_Ldap_Exception('Could not connect to ' .  $host . ':' . $this->_config['port']);
 314:                 $this->_downHostList[] = $host;
 315:                 continue;
 316:             }
 317: 
 318:             /* If we're supposed to use TLS, do so before we try to bind, as
 319:              * some strict servers only allow binding via secure
 320:              * connections. */
 321:             if ($this->_config['tls']) {
 322:                 try {
 323:                     $this->startTLS();
 324:                 } catch (Horde_Ldap_Exception $e) {
 325:                     $current_error           = $e;
 326:                     $this->_link             = false;
 327:                     $this->_downHostList[] = $host;
 328:                     continue;
 329:                 }
 330:             }
 331: 
 332:             /* Try to set the configured LDAP version on the connection if LDAP
 333:              * server needs that before binding (eg OpenLDAP).
 334:              * This could be necessary since RFC 1777 states that the protocol
 335:              * version has to be set at the bind request.
 336:              * We use force here which means that the test in the rootDSE is
 337:              * skipped; this is neccessary, because some strict LDAP servers
 338:              * only allow to read the LDAP rootDSE (which tells us the
 339:              * supported protocol versions) with authenticated clients.
 340:              * This may fail in which case we try again after binding.
 341:              * In this case, most probably the bind() or setVersion() call
 342:              * below will also fail, providing error messages. */
 343:             $version_set = false;
 344:             $this->setVersion(0, true);
 345: 
 346:             /* Attempt to bind to the server. If we have credentials
 347:              * configured, we try to use them, otherwise it's an anonymous
 348:              * bind.
 349:              * As stated by RFC 1777, the bind request should be the first
 350:              * operation to be performed after the connection is established.
 351:              * This may give an protocol error if the server does not support
 352:              * v2 binds and the above call to setVersion() failed.
 353:              * If the above call failed, we try an v2 bind here and set the
 354:              * version afterwards (with checking to the rootDSE). */
 355:             try {
 356:                 $this->bind();
 357:             } catch (Exception $e) {
 358:                 /* The bind failed, discard link and save error msg.
 359:                  * Then record the host as down and try next one. */
 360:                 if ($this->errorName($e->getCode()) == 'LDAP_PROTOCOL_ERROR' &&
 361:                     !$version_set) {
 362:                     /* Provide a finer grained error message if protocol error
 363:                      * arises because of invalid version. */
 364:                     $e = new Horde_Ldap_Exception($e->getMessage() . ' (could not set LDAP protocol version to ' . $this->_config['version'].')', $e->getCode());
 365:                 }
 366:                 $this->_link             = false;
 367:                 $current_error           = $e;
 368:                 $this->_downHostList[] = $host;
 369:                 continue;
 370:             }
 371: 
 372:             /* Set desired LDAP version if not successfully set before.
 373:              * Here, a check against the rootDSE is performed, so we get a
 374:              * error message if the server does not support the version.
 375:              * The rootDSE entry should tell us which LDAP versions are
 376:              * supported. However, some strict LDAP servers only allow
 377:              * bound users to read the rootDSE. */
 378:             if (!$version_set) {
 379:                 try {
 380:                     $this->setVersion();
 381:                 } catch (Exception $e) {
 382:                     $current_error           = $e;
 383:                     $this->_link             = false;
 384:                     $this->_downHostList[] = $host;
 385:                     continue;
 386:                 }
 387:             }
 388: 
 389:             /* Set LDAP parameters, now that we know we have a valid
 390:              * connection. */
 391:             if (isset($this->_config['options']) &&
 392:                 is_array($this->_config['options']) &&
 393:                 count($this->_config['options'])) {
 394:                 foreach ($this->_config['options'] as $opt => $val) {
 395:                     try {
 396:                         $this->setOption($opt, $val);
 397:                     } catch (Exception $e) {
 398:                         $current_error           = $e;
 399:                         $this->_link             = false;
 400:                         $this->_downHostList[] = $host;
 401:                         continue 2;
 402:                     }
 403:                 }
 404:             }
 405: 
 406:             /* At this stage we have connected, bound, and set up options, so
 407:              * we have a known good LDAP server.  Time to go home. */
 408:             return;
 409:         }
 410: 
 411:         /* All connection attempts have failed, return the last error. */
 412:         throw $current_error;
 413:     }
 414: 
 415:     /**
 416:      * Reconnects to the LDAP server.
 417:      *
 418:      * In case the connection to the LDAP service has dropped out for some
 419:      * reason, this function will reconnect, and re-bind if a bind has been
 420:      * attempted in the past.  It is probably most useful when the server list
 421:      * provided to the new() or _connect() function is an array rather than a
 422:      * single host name, because in that case it will be able to connect to a
 423:      * failover or secondary server in case the primary server goes down.
 424:      *
 425:      * This method just tries to re-establish the current connection.  It will
 426:      * sleep for the current backoff period (seconds) before attempting the
 427:      * connect, and if the connection fails it will double the backoff period,
 428:      * but not try again.  If you want to ensure a reconnection during a
 429:      * transient period of server downtime then you need to call this function
 430:      * in a loop.
 431:      *
 432:      * @throws Horde_Ldap_Exception
 433:      */
 434:     protected function _reconnect()
 435:     {
 436:         /* Return if we are already connected. */
 437:         if ($this->_link) {
 438:             return;
 439:         }
 440: 
 441:         /* Sleep for a backoff period in seconds. */
 442:         sleep($this->_config['current_backoff']);
 443: 
 444:         /* Retry all available connections. */
 445:         $this->_downHostList = array();
 446: 
 447:         try {
 448:             $this->_connect();
 449:         } catch (Horde_Ldap_Exception $e) {
 450:             $this->_config['current_backoff'] *= 2;
 451:             if ($this->_config['current_backoff'] > $this->_config['max_backoff']) {
 452:                 $this->_config['current_backoff'] = $this->_config['max_backoff'];
 453:             }
 454:             throw $e;
 455:         }
 456: 
 457:         /* Now we should be able to safely (re-)bind. */
 458:         try {
 459:             $this->bind();
 460:         } catch (Exception $e) {
 461:             $this->_config['current_backoff'] *= 2;
 462:             if ($this->_config['current_backoff'] > $this->_config['max_backoff']) {
 463:                 $this->_config['current_backoff'] = $this->_config['max_backoff'];
 464:             }
 465: 
 466:             /* $this->_config['hostspec'] should have had the last connected
 467:              * host stored in it by _connect().  Since we are unable to
 468:              * bind to that host we can safely assume that it is down or has
 469:              * some other problem. */
 470:             $this->_downHostList[] = $this->_config['hostspec'];
 471:             throw $e;
 472:         }
 473: 
 474:         /* At this stage we have connected, bound, and set up options, so we
 475:          * have a known good LDAP server. Time to go home. */
 476:         $this->_config['current_backoff'] = $this->_config['min_backoff'];
 477:     }
 478: 
 479:     /**
 480:      * Closes the LDAP connection.
 481:      */
 482:     public function disconnect()
 483:     {
 484:         @ldap_close($this->_link);
 485:     }
 486: 
 487:     /**
 488:      * Starts an encrypted session.
 489:      *
 490:      * @throws Horde_Ldap_Exception
 491:      */
 492:     public function startTLS()
 493:     {
 494:         /* Test to see if the server supports TLS first.
 495:          * This is done via testing the extensions offered by the server.
 496:          * The OID 1.3.6.1.4.1.1466.20037 tells whether TLS is supported. */
 497:         try {
 498:             $rootDSE = $this->rootDSE();
 499:         } catch (Exception $e) {
 500:             throw new Horde_Ldap_Exception('Unable to fetch rootDSE entry to see if TLS is supported: ' . $e->getMessage(), $e->getCode());
 501:         }
 502: 
 503:         try {
 504:             $supported_extensions = $rootDSE->getValue('supportedExtension');
 505:         } catch (Exception $e) {
 506:             throw new Horde_Ldap_Exception('Unable to fetch rootDSE attribute "supportedExtension" to see if TLS is supoported: ' . $e->getMessage(), $e->getCode());
 507:         }
 508: 
 509:         if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) {
 510:             throw new Horde_Ldap_Exception('Server reports that it does not support TLS');
 511:         }
 512: 
 513:         if (!@ldap_start_tls($this->_link)) {
 514:             throw new Horde_Ldap_Exception('TLS not started: ' . @ldap_error($this->_link),
 515:                                            @ldap_errno($this->_link));
 516:         }
 517:     }
 518: 
 519:     /**
 520:      * Adds a new entry to the directory.
 521:      *
 522:      * This also links the entry to the connection used for the add, if it was
 523:      * a fresh entry.
 524:      *
 525:      * @see HordeLdap_Entry::createFresh()
 526:      *
 527:      * @param Horde_Ldap_Entry $entry An LDAP entry.
 528:      *
 529:      * @throws Horde_Ldap_Exception
 530:      */
 531:     public function add(Horde_Ldap_Entry $entry)
 532:     {
 533:         /* Continue attempting the add operation in a loop until we get a
 534:          * success, a definitive failure, or the world ends. */
 535:         while (true) {
 536:             $link = $this->getLink();
 537:             if ($link === false) {
 538:                 /* We do not have a successful connection yet.  The call to
 539:                  * getLink() would have kept trying if we wanted one. */
 540:                 throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ' no valid LDAP connection could be found.');
 541:             }
 542: 
 543:             if (@ldap_add($link, $entry->dn(), $entry->getValues())) {
 544:                 /* Entry successfully added, we should update its Horde_Ldap
 545:                  * reference in case it is not set so far (fresh entry). */
 546:                 try {
 547:                     $entry->getLDAP();
 548:                 } catch (Horde_Ldap_Exception $e) {
 549:                     $entry->setLDAP($this);
 550:                 }
 551:                 /* Store that the entry is present inside the directory. */
 552:                 $entry->markAsNew(false);
 553:                 return;
 554:             }
 555: 
 556:             /* We have a failure.  What kind?  We may be able to reconnect and
 557:              * try again. */
 558:             $error_code = @ldap_errno($link);
 559:             if ($this->errorName($error_code) != 'LDAP_OPERATIONS_ERROR' |
 560:                 !$this->_config['auto_reconnect']) {
 561:                 /* Errors other than the above are just passed back to the user
 562:                  * so he may react upon them. */
 563:                 throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ': ' . ldap_err2str($error_code), $error_code);
 564:             }
 565: 
 566:             /* The server has disconnected before trying the operation.  We
 567:              * should try again, possibly with a different server. */
 568:             $this->_link = false;
 569:             $this->_reconnect();
 570:         }
 571:     }
 572: 
 573:     /**
 574:      * Deletes an entry from the directory.
 575:      *
 576:      * @param string|Horde_Ldap_Entry $dn        DN string or Horde_Ldap_Entry.
 577:      * @param boolean                 $recursive Should we delete all children
 578:      *                                           recursivelx as well?
 579:      * @throws Horde_Ldap_Exception
 580:      */
 581:     public function delete($dn, $recursive = false)
 582:     {
 583:         if ($dn instanceof Horde_Ldap_Entry) {
 584:              $dn = $dn->dn();
 585:         }
 586:         if (!is_string($dn)) {
 587:             throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!');
 588:         }
 589: 
 590:         /* Recursive delete searches for children and calls delete for them. */
 591:         if ($recursive) {
 592:             $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0);
 593:             if ($result && @ldap_count_entries($this->_link, $result)) {
 594:                 for ($subentry = @ldap_first_entry($this->_link, $result);
 595:                      $subentry;
 596:                      $subentry = @ldap_next_entry($this->_link, $subentry)) {
 597:                     $this->delete(@ldap_get_dn($this->_link, $subentry), true);
 598:                 }
 599:             }
 600:         }
 601: 
 602:         /* Continue the delete operation in a loop until we get a success, or a
 603:          * definitive failure. */
 604:         while (true) {
 605:             $link = $this->getLink();
 606:             if (!$link) {
 607:                 /* We do not have a successful connection yet.  The call to
 608:                  * getLink() would have kept trying if we wanted one. */
 609:                 throw new Horde_Ldap_Exception('Could not add entry ' . $dn . ' no valid LDAP connection could be found.');
 610:             }
 611: 
 612:             $s = @ldap_delete($link, $dn);
 613:             if ($s) {
 614:                 /* Entry successfully deleted. */
 615:                 return;
 616:             }
 617: 
 618:             /* We have a failure.  What kind? We may be able to reconnect and
 619:              * try again. */
 620:             $error_code = @ldap_errno($link);
 621:             if ($this->errorName($error_code) == 'LDAP_OPERATIONS_ERROR' &&
 622:                 $this->_config['auto_reconnect']) {
 623:                 /* The server has disconnected before trying the operation.  We
 624:                  * should try again, possibly with a different server. */
 625:                 $this->_link = false;
 626:                 $this->_reconnect();
 627:             } elseif ($this->errorName($error_code) == 'LDAP_NOT_ALLOWED_ON_NONLEAF') {
 628:                 /* Subentries present, server refused to delete.
 629:                  * Deleting subentries is the clients responsibility, but since
 630:                  * the user may not know of the subentries, we do not force
 631:                  * that here but instead notify the developer so he may take
 632:                  * actions himself. */
 633:                 throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ' because of subentries. Use the recursive parameter to delete them.', $error_code);
 634:             } else {
 635:                 /* Errors other than the above catched are just passed back to
 636:                  * the user so he may react upon them. */
 637:                 throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ': ' . ldap_err2str($error_code), $error_code);
 638:             }
 639:         }
 640:     }
 641: 
 642:     /**
 643:      * Modifies an LDAP entry on the server.
 644:      *
 645:      * The $params argument is an array of actions and should be something like
 646:      * this:
 647:      * <code>
 648:      * array('add' => array('attribute1' => array('val1', 'val2'),
 649:      *                      'attribute2' => array('val1')),
 650:      *       'delete' => array('attribute1'),
 651:      *       'replace' => array('attribute1' => array('val1')),
 652:      *       'changes' => array('add' => ...,
 653:      *                          'replace' => ...,
 654:      *                          'delete' => array('attribute1', 'attribute2' => array('val1')))
 655:      * </code>
 656:      *
 657:      * The order of execution is as following:
 658:      *   1. adds from 'add' array
 659:      *   2. deletes from 'delete' array
 660:      *   3. replaces from 'replace' array
 661:      *   4. changes (add, replace, delete) in order of appearance
 662:      *
 663:      * The function calls the corresponding functions of an Horde_Ldap_Entry
 664:      * object. A detailed description of array structures can be found there.
 665:      *
 666:      * Unlike the modification methods provided by the Horde_Ldap_Entry object,
 667:      * this method will instantly carry out an update() after each operation,
 668:      * thus modifying "directly" on the server.
 669:      *
 670:      * @see Horde_Ldap_Entry::add()
 671:      * @see Horde_Ldap_Entry::delete()
 672:      * @see Horde_Ldap_Entry::replace()
 673:      *
 674:      * @param string|Horde_Ldap_Entry $entry DN string or Horde_Ldap_Entry.
 675:      * @param array                   $parms Array of changes
 676:      *
 677:      * @throws Horde_Ldap_Exception
 678:      */
 679:     public function modify($entry, $parms = array())
 680:     {
 681:         if (is_string($entry)) {
 682:             $entry = $this->getEntry($entry);
 683:         }
 684:         if (!($entry instanceof Horde_Ldap_Entry)) {
 685:             throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!');
 686:         }
 687: 
 688:         /* Perform changes mentioned separately. */
 689:         foreach (array('add', 'delete', 'replace') as $action) {
 690:             if (!isset($parms[$action])) {
 691:                 continue;
 692:             }
 693:             $entry->$action($parms[$action]);
 694:             $entry->setLDAP($this);
 695: 
 696:             /* Because the ldap_*() functions are called inside
 697:              * Horde_Ldap_Entry::update(), we have to trap the error codes
 698:              * issued from that if we want to support reconnection. */
 699:             while (true) {
 700:                 try {
 701:                     $entry->update();
 702:                     break;
 703:                 } catch (Exception $e) {
 704:                     /* We have a failure.  What kind?  We may be able to
 705:                      * reconnect and try again. */
 706:                     if ($this->errorName($e->getCode()) != 'LDAP_OPERATIONS_ERROR' ||
 707:                         !$this->_config['auto_reconnect']) {
 708:                         /* Errors other than the above catched are just passed
 709:                          * back to the user so he may react upon them. */
 710:                         throw new Horde_Ldap_Exception('Could not modify entry: ' . $e->getMessage());
 711:                     }
 712:                     /* The server has disconnected before trying the operation.
 713:                      * We should try again, possibly with a different
 714:                      * server. */
 715:                     $this->_link = false;
 716:                     $this->_reconnect();
 717:                 }
 718:             }
 719:         }
 720: 
 721:         if (!isset($parms['changes']) || !is_array($parms['changes'])) {
 722:             return;
 723:         }
 724: 
 725:         /* Perform combined changes in 'changes' array. */
 726:         foreach ($parms['changes'] as $action => $value) {
 727:             $this->modify($entry, array($action => $value));
 728:         }
 729:     }
 730: 
 731:     /**
 732:      * Runs an LDAP search query.
 733:      *
 734:      * $base and $filter may be ommitted. The one from config will then be
 735:      * used. $base is either a DN-string or an Horde_Ldap_Entry object in which
 736:      * case its DN will be used.
 737:      *
 738:      * $params may contain:
 739:      * - scope: The scope which will be used for searching, defaults to 'sub':
 740:      *          - base: Just one entry
 741:      *          - sub: The whole tree
 742:      *          - one: Immediately below $base
 743:      * - sizelimit: Limit the number of entries returned
 744:      *              (default: 0 = unlimited)
 745:      * - timelimit: Limit the time spent for searching (default: 0 = unlimited)
 746:      * - attrsonly: If true, the search will only return the attribute names
 747:      * - attributes: Array of attribute names, which the entry should contain.
 748:      *               It is good practice to limit this to just the ones you
 749:      *               need.
 750:      *
 751:      * You cannot override server side limitations to sizelimit and timelimit:
 752:      * You can always only lower a given limit.
 753:      *
 754:      * @todo implement search controls (sorting etc)
 755:      *
 756:      * @param string|Horde_Ldap_Entry  $base   LDAP searchbase.
 757:      * @param string|Horde_Ldap_Filter $filter LDAP search filter.
 758:      * @param array                    $params Array of options.
 759:      *
 760:      * @return Horde_Ldap_Search  The search result.
 761:      * @throws Horde_Ldap_Exception
 762:      */
 763:     public function search($base = null, $filter = null, $params = array())
 764:     {
 765:         if (is_null($base)) {
 766:             $base = $this->_config['basedn'];
 767:         }
 768:         if ($base instanceof Horde_Ldap_Entry) {
 769:             /* Fetch DN of entry, making searchbase relative to the entry. */
 770:             $base = $base->dn();
 771:         }
 772:         if (is_null($filter)) {
 773:             $filter = $this->_config['filter'];
 774:         }
 775:         if ($filter instanceof Horde_Ldap_Filter) {
 776:             /* Convert Horde_Ldap_Filter to string representation. */
 777:             $filter = (string)$filter;
 778:         }
 779: 
 780:         /* Setting search parameters.  */
 781:         $sizelimit  = isset($params['sizelimit']) ? $params['sizelimit'] : 0;
 782:         $timelimit  = isset($params['timelimit']) ? $params['timelimit'] : 0;
 783:         $attrsonly  = isset($params['attrsonly']) ? $params['attrsonly'] : 0;
 784:         $attributes = isset($params['attributes']) ? $params['attributes'] : array();
 785: 
 786:         /* Ensure $attributes to be an array in case only one attribute name
 787:          * was given as string. */
 788:         if (!is_array($attributes)) {
 789:             $attributes = array($attributes);
 790:         }
 791: 
 792:         /* Reorganize the $attributes array index keys sometimes there are
 793:          * problems with not consecutive indexes. */
 794:         $attributes = array_values($attributes);
 795: 
 796:         /* Scoping makes searches faster! */
 797:         $scope = isset($params['scope'])
 798:             ? $params['scope']
 799:             : $this->_config['scope'];
 800: 
 801:         switch ($scope) {
 802:         case 'one':
 803:             $search_function = 'ldap_list';
 804:             break;
 805:         case 'base':
 806:             $search_function = 'ldap_read';
 807:             break;
 808:         default:
 809:             $search_function = 'ldap_search';
 810:         }
 811: 
 812:         /* Continue attempting the search operation until we get a success or a
 813:          * definitive failure. */
 814:         while (true) {
 815:             $link = $this->getLink();
 816:             $search = @call_user_func($search_function,
 817:                                       $link,
 818:                                       $base,
 819:                                       $filter,
 820:                                       $attributes,
 821:                                       $attrsonly,
 822:                                       $sizelimit,
 823:                                       $timelimit);
 824: 
 825:             if ($errno = @ldap_errno($link)) {
 826:                 $err = $this->errorName($errno);
 827:                 if ($err == 'LDAP_NO_SUCH_OBJECT' ||
 828:                     $err == 'LDAP_SIZELIMIT_EXCEEDED') {
 829:                     return new Horde_Ldap_Search($search, $this, $attributes);
 830:                 }
 831:                 if ($err == 'LDAP_FILTER_ERROR') {
 832:                     /* Bad search filter. */
 833:                     throw new Horde_Ldap_Exception(ldap_err2str($errno) . ' ($filter)', $errno);
 834:                 }
 835:                 if ($err == 'LDAP_OPERATIONS_ERROR' &&
 836:                     $this->_config['auto_reconnect']) {
 837:                     $this->_link = false;
 838:                     $this->_reconnect();
 839:                 } else {
 840:                     $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope";
 841:                     throw new Horde_Ldap_Exception(ldap_err2str($errno) . $msg, $errno);
 842:                 }
 843:             } else {
 844:                 return new Horde_Ldap_Search($search, $this, $attributes);
 845:             }
 846:         }
 847:     }
 848: 
 849:     /**
 850:      * Returns the DN of a user.
 851:      *
 852:      * The purpose is to quickly find the full DN of a user so it can be used
 853:      * to re-bind as this user. This method requires the 'user' configuration
 854:      * parameter to be set.
 855:      *
 856:      * @param string $user  The user to find.
 857:      *
 858:      * @return string  The user's full DN.
 859:      * @throws Horde_Ldap_Exception
 860:      * @throws Horde_Exception_NotFound
 861:      */
 862:     public function findUserDN($user)
 863:     {
 864:         $filter = Horde_Ldap_Filter::combine(
 865:             'and',
 866:             array(Horde_Ldap_Filter::build($this->_config['user']),
 867:                   Horde_Ldap_Filter::create($this->_config['user']['uid'], 'equals', $user)));
 868:         $search = $this->search(
 869:             null,
 870:             $filter,
 871:             array('attributes' => array($this->_config['user']['uid'])));
 872:         if (!$search->count()) {
 873:             throw new Horde_Exception_NotFound('DN for user ' . $user . ' not found');
 874:         }
 875:         $entry = $search->shiftEntry();
 876:         return $entry->currentDN();
 877:     }
 878: 
 879:     /**
 880:      * Sets an LDAP option.
 881:      *
 882:      * @param string $option Option to set.
 883:      * @param mixed  $value  Value to set option to.
 884:      *
 885:      * @throws Horde_Ldap_Exception
 886:      */
 887:     public function setOption($option, $value)
 888:     {
 889:         if (!$this->_link) {
 890:             throw new Horde_Ldap_Exception('Could not set LDAP option: No LDAP connection');
 891:         }
 892:         if (!defined($option)) {
 893:             throw new Horde_Ldap_Exception('Unkown option requested');
 894:         }
 895:         if (@ldap_set_option($this->_link, constant($option), $value)) {
 896:             return;
 897:         }
 898:         $err = @ldap_errno($this->_link);
 899:         if ($err) {
 900:             throw new Horde_Ldap_Exception(ldap_err2str($err), $err);
 901:         }
 902:         throw new Horde_Ldap_Exception('Unknown error');
 903:     }
 904: 
 905:     /**
 906:      * Returns an LDAP option value.
 907:      *
 908:      * @param string $option Option to get.
 909:      *
 910:      * @return Horde_Ldap_Error|string Horde_Ldap_Error or option value
 911:      * @throws Horde_Ldap_Exception
 912:      */
 913:     public function getOption($option)
 914:     {
 915:         if (!$this->_link) {
 916:             throw new Horde_Ldap_Exception('No LDAP connection');
 917:         }
 918:         if (!defined($option)) {
 919:             throw new Horde_Ldap_Exception('Unkown option requested');
 920:         }
 921:         if (@ldap_get_option($this->_link, constant($option), $value)) {
 922:             return $value;
 923:         }
 924:         $err = @ldap_errno($this->_link);
 925:         if ($err) {
 926:             throw new Horde_Ldap_Exception(ldap_err2str($err), $err);
 927:         }
 928:         throw new Horde_Ldap_Exception('Unknown error');
 929:     }
 930: 
 931:     /**
 932:      * Returns the LDAP protocol version that is used on the connection.
 933:      *
 934:      * A lot of LDAP functionality is defined by what protocol version
 935:      * the LDAP server speaks. This might be 2 or 3.
 936:      *
 937:      * @return integer  The protocol version.
 938:      */
 939:     public function getVersion()
 940:     {
 941:         if ($this->_link) {
 942:             $version = $this->getOption('LDAP_OPT_PROTOCOL_VERSION');
 943:         } else {
 944:             $version = $this->_config['version'];
 945:         }
 946:         return $version;
 947:     }
 948: 
 949:     /**
 950:      * Sets the LDAP protocol version that is used on the connection.
 951:      *
 952:      * @todo Checking via the rootDSE takes much time - why? fetching
 953:      *       and instanciation is quick!
 954:      *
 955:      * @param integer $version LDAP version that should be used.
 956:      * @param boolean $force   If set to true, the check against the rootDSE
 957:      *                         will be skipped.
 958:      *
 959:      * @throws Horde_Ldap_Exception
 960:      */
 961:     public function setVersion($version = 0, $force = false)
 962:     {
 963:         if (!$version) {
 964:             $version = $this->_config['version'];
 965:         }
 966: 
 967:         /* Check to see if the server supports this version first.
 968:          *
 969:          * TODO: Why is this so horribly slow? $this->rootDSE() is very fast,
 970:          * as well as Horde_Ldap_RootDse(). Seems like a problem at copying the
 971:          * object inside PHP??  Additionally, this is not always
 972:          * reproducable... */
 973:         if (!$force) {
 974:             try {
 975:                 $rootDSE = $this->rootDSE();
 976:                 $supported_versions = $rootDSE->getValue('supportedLDAPVersion');
 977:                 if (is_string($supported_versions)) {
 978:                     $supported_versions = array($supported_versions);
 979:                 }
 980:                 $check_ok = in_array($version, $supported_versions);
 981:             } catch (Horde_Ldap_Exception $e) {
 982:                 /* If we don't get a root DSE, this is probably a v2 server. */
 983:                 $check_ok = $version < 3;
 984:             }
 985:         }
 986:         $check_ok = true;
 987: 
 988:         if ($force || $check_ok) {
 989:             return $this->setOption('LDAP_OPT_PROTOCOL_VERSION', $version);
 990:         }
 991:         throw new Horde_Ldap_Exception('LDAP Server does not support protocol version ' . $version);
 992:     }
 993: 
 994: 
 995:     /**
 996:      * Returns whether a DN exists in the directory.
 997:      *
 998:      * @param string|Horde_Ldap_Entry $dn The DN of the object to test.
 999:      *
1000:      * @return boolean  True if the DN exists.
1001:      * @throws Horde_Ldap_Exception
1002:      */
1003:     public function exists($dn)
1004:     {
1005:         if ($dn instanceof Horde_Ldap_Entry) {
1006:              $dn = $dn->dn();
1007:         }
1008:         if (!is_string($dn)) {
1009:             throw new Horde_Ldap_Exception('Parameter $dn is not a string nor an entry object!');
1010:         }
1011: 
1012:         /* Make dn relative to parent. */
1013:         $base = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false));
1014:         $entry_rdn = array_shift($base);
1015:         $base = Horde_Ldap_Util::canonicalDN($base);
1016: 
1017:         $result = @ldap_list($this->_link, $base, $entry_rdn, array(), 1, 1);
1018:         if (@ldap_count_entries($this->_link, $result)) {
1019:             return true;
1020:         }
1021:         if ($this->errorName(@ldap_errno($this->_link)) == 'LDAP_NO_SUCH_OBJECT') {
1022:             return false;
1023:         }
1024:         if (@ldap_errno($this->_link)) {
1025:             throw new Horde_Ldap_Exception(@ldap_error($this->_link), @ldap_errno($this->_link));
1026:         }
1027:         return false;
1028:     }
1029: 
1030: 
1031:     /**
1032:      * Returns a specific entry based on the DN.
1033:      *
1034:      * @todo Maybe a check against the schema should be done to be
1035:      *       sure the attribute type exists.
1036:      *
1037:      * @param string $dn   DN of the entry that should be fetched.
1038:      * @param array  $attributes Array of Attributes to select. If ommitted, all
1039:      *                     attributes are fetched.
1040:      *
1041:      * @return Horde_Ldap_Entry  A Horde_Ldap_Entry object.
1042:      * @throws Horde_Ldap_Exception
1043:      * @throws Horde_Exception_NotFound
1044:      */
1045:     public function getEntry($dn, $attributes = array())
1046:     {
1047:         if (!is_array($attributes)) {
1048:             $attributes = array($attributes);
1049:         }
1050:         $result = $this->search($dn, '(objectClass=*)',
1051:                                 array('scope' => 'base', 'attributes' => $attributes));
1052:         if (!$result->count()) {
1053:             throw new Horde_Exception_NotFound(sprintf('Could not fetch entry %s: no entry found', $dn));
1054:         }
1055:         $entry = $result->shiftEntry();
1056:         if (!$entry) {
1057:             throw new Horde_Ldap_Exception('Could not fetch entry (error retrieving entry from search result)');
1058:         }
1059:         return $entry;
1060:     }
1061: 
1062:     /**
1063:      * Renames or moves an entry.
1064:      *
1065:      * This method will instantly carry out an update() after the
1066:      * move, so the entry is moved instantly.
1067:      *
1068:      * You can pass an optional Horde_Ldap object. In this case, a
1069:      * cross directory move will be performed which deletes the entry
1070:      * in the source (THIS) directory and adds it in the directory
1071:      * $target_ldap.
1072:      *
1073:      * A cross directory move will switch the entry's internal LDAP
1074:      * reference so updates to the entry will go to the new directory.
1075:      *
1076:      * If you want to do a cross directory move, you need to pass an
1077:      * Horde_Ldap_Entry object, otherwise the attributes will be
1078:      * empty.
1079:      *
1080:      * @param string|Horde_Ldap_Entry $entry       An LDAP entry.
1081:      * @param string                  $newdn       The new location.
1082:      * @param Horde_Ldap              $target_ldap Target directory for cross
1083:      *                                             server move.
1084:      *
1085:      * @throws Horde_Ldap_Exception
1086:      */
1087:     public function move($entry, $newdn, $target_ldap = null)
1088:     {
1089:         if (is_string($entry)) {
1090:             if ($target_ldap && $target_ldap !== $this) {
1091:                 throw new Horde_Ldap_Exception('Unable to perform cross directory move: operation requires a Horde_Ldap_Entry object');
1092:             }
1093:             $entry = $this->getEntry($entry);
1094:         }
1095:         if (!$entry instanceof Horde_Ldap_Entry) {
1096:             throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object! (If DN was passed, conversion failed)');
1097:         }
1098:         if ($target_ldap && !($target_ldap instanceof Horde_Ldap)) {
1099:             throw new Horde_Ldap_Exception('Parameter $target_ldap is expected to be a Horde_Ldap object!');
1100:         }
1101: 
1102:         if (!$target_ldap || $target_ldap === $this) {
1103:             /* Local move. */
1104:             $entry->dn($newdn);
1105:             $entry->setLDAP($this);
1106:             $entry->update();
1107:             return;
1108:         }
1109: 
1110:         /* Cross directory move. */
1111:         if ($target_ldap->exists($newdn)) {
1112:             throw new Horde_Ldap_Exception('Unable to perform cross directory move: entry does exist in target directory');
1113:         }
1114:         $entry->dn($newdn);
1115:         try {
1116:             $target_ldap->add($entry);
1117:         } catch (Exception $e) {
1118:             throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in target directory');
1119:         }
1120: 
1121:         try {
1122:             $this->delete($entry->currentDN());
1123:         } catch (Exception $e) {
1124:             try {
1125:                 $add_error_string = '';
1126:                 /* Undo add. */
1127:                 $target_ldap->delete($entry);
1128:             } catch (Exception $e) {
1129:                 $add_error_string = ' Additionally, the deletion (undo add) of $entry in target directory failed.';
1130:             }
1131:             throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in source directory.' . $add_error_string);
1132:         }
1133:         $entry->setLDAP($target_ldap);
1134:     }
1135: 
1136:     /**
1137:      * Copies an entry to a new location.
1138:      *
1139:      * The entry will be immediately copied. Only attributes you have
1140:      * selected will be copied.
1141:      *
1142:      * @param Horde_Ldap_Entry $entry An LDAP entry.
1143:      * @param string           $newdn New FQF-DN of the entry.
1144:      *
1145:      * @return Horde_Ldap_Entry  The copied entry.
1146:      * @throws Horde_Ldap_Exception
1147:      */
1148:     public function copy($entry, $newdn)
1149:     {
1150:         if (!$entry instanceof Horde_Ldap_Entry) {
1151:             throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object');
1152:         }
1153: 
1154:         $newentry = Horde_Ldap_Entry::createFresh($newdn, $entry->getValues());
1155:         $this->add($newentry);
1156: 
1157:         return $newentry;
1158:     }
1159: 
1160: 
1161:     /**
1162:      * Returns the string for an LDAP errorcode.
1163:      *
1164:      * Made to be able to make better errorhandling.  Function based
1165:      * on DB::errorMessage().
1166:      *
1167:      * Hint: The best description of the errorcodes is found here:
1168:      * http://www.directory-info.com/Ldap/LDAPErrorCodes.html
1169:      *
1170:      * @param integer $errorcode An error code.
1171:      *
1172:      * @return string The description for the error.
1173:      */
1174:     public static function errorName($errorcode)
1175:     {
1176:         $errorMessages = array(
1177:             0x00 => 'LDAP_SUCCESS',
1178:             0x01 => 'LDAP_OPERATIONS_ERROR',
1179:             0x02 => 'LDAP_PROTOCOL_ERROR',
1180:             0x03 => 'LDAP_TIMELIMIT_EXCEEDED',
1181:             0x04 => 'LDAP_SIZELIMIT_EXCEEDED',
1182:             0x05 => 'LDAP_COMPARE_FALSE',
1183:             0x06 => 'LDAP_COMPARE_TRUE',
1184:             0x07 => 'LDAP_AUTH_METHOD_NOT_SUPPORTED',
1185:             0x08 => 'LDAP_STRONG_AUTH_REQUIRED',
1186:             0x09 => 'LDAP_PARTIAL_RESULTS',
1187:             0x0a => 'LDAP_REFERRAL',
1188:             0x0b => 'LDAP_ADMINLIMIT_EXCEEDED',
1189:             0x0c => 'LDAP_UNAVAILABLE_CRITICAL_EXTENSION',
1190:             0x0d => 'LDAP_CONFIDENTIALITY_REQUIRED',
1191:             0x0e => 'LDAP_SASL_BIND_INPROGRESS',
1192:             0x10 => 'LDAP_NO_SUCH_ATTRIBUTE',
1193:             0x11 => 'LDAP_UNDEFINED_TYPE',
1194:             0x12 => 'LDAP_INAPPROPRIATE_MATCHING',
1195:             0x13 => 'LDAP_CONSTRAINT_VIOLATION',
1196:             0x14 => 'LDAP_TYPE_OR_VALUE_EXISTS',
1197:             0x15 => 'LDAP_INVALID_SYNTAX',
1198:             0x20 => 'LDAP_NO_SUCH_OBJECT',
1199:             0x21 => 'LDAP_ALIAS_PROBLEM',
1200:             0x22 => 'LDAP_INVALID_DN_SYNTAX',
1201:             0x23 => 'LDAP_IS_LEAF',
1202:             0x24 => 'LDAP_ALIAS_DEREF_PROBLEM',
1203:             0x30 => 'LDAP_INAPPROPRIATE_AUTH',
1204:             0x31 => 'LDAP_INVALID_CREDENTIALS',
1205:             0x32 => 'LDAP_INSUFFICIENT_ACCESS',
1206:             0x33 => 'LDAP_BUSY',
1207:             0x34 => 'LDAP_UNAVAILABLE',
1208:             0x35 => 'LDAP_UNWILLING_TO_PERFORM',
1209:             0x36 => 'LDAP_LOOP_DETECT',
1210:             0x3C => 'LDAP_SORT_CONTROL_MISSING',
1211:             0x3D => 'LDAP_INDEX_RANGE_ERROR',
1212:             0x40 => 'LDAP_NAMING_VIOLATION',
1213:             0x41 => 'LDAP_OBJECT_CLASS_VIOLATION',
1214:             0x42 => 'LDAP_NOT_ALLOWED_ON_NONLEAF',
1215:             0x43 => 'LDAP_NOT_ALLOWED_ON_RDN',
1216:             0x44 => 'LDAP_ALREADY_EXISTS',
1217:             0x45 => 'LDAP_NO_OBJECT_CLASS_MODS',
1218:             0x46 => 'LDAP_RESULTS_TOO_LARGE',
1219:             0x47 => 'LDAP_AFFECTS_MULTIPLE_DSAS',
1220:             0x50 => 'LDAP_OTHER',
1221:             0x51 => 'LDAP_SERVER_DOWN',
1222:             0x52 => 'LDAP_LOCAL_ERROR',
1223:             0x53 => 'LDAP_ENCODING_ERROR',
1224:             0x54 => 'LDAP_DECODING_ERROR',
1225:             0x55 => 'LDAP_TIMEOUT',
1226:             0x56 => 'LDAP_AUTH_UNKNOWN',
1227:             0x57 => 'LDAP_FILTER_ERROR',
1228:             0x58 => 'LDAP_USER_CANCELLED',
1229:             0x59 => 'LDAP_PARAM_ERROR',
1230:             0x5a => 'LDAP_NO_MEMORY',
1231:             0x5b => 'LDAP_CONNECT_ERROR',
1232:             0x5c => 'LDAP_NOT_SUPPORTED',
1233:             0x5d => 'LDAP_CONTROL_NOT_FOUND',
1234:             0x5e => 'LDAP_NO_RESULTS_RETURNED',
1235:             0x5f => 'LDAP_MORE_RESULTS_TO_RETURN',
1236:             0x60 => 'LDAP_CLIENT_LOOP',
1237:             0x61 => 'LDAP_REFERRAL_LIMIT_EXCEEDED',
1238:             1000 => 'Unknown Error');
1239: 
1240:          return isset($errorMessages[$errorcode]) ?
1241:             $errorMessages[$errorcode] :
1242:             'Unknown Error (' . $errorcode . ')';
1243:     }
1244: 
1245:     /**
1246:      * Returns a rootDSE object
1247:      *
1248:      * This either fetches a fresh rootDSE object or returns it from
1249:      * the internal cache for performance reasons, if possible.
1250:      *
1251:      * @param array $attrs Array of attributes to search for.
1252:      *
1253:      * @return Horde_Ldap_RootDse Horde_Ldap_RootDse object
1254:      * @throws Horde_Ldap_Exception
1255:      */
1256:     public function rootDSE(array $attrs = array())
1257:     {
1258:         $attrs_signature = serialize($attrs);
1259: 
1260:         /* See if we need to fetch a fresh object, or if we already
1261:          * requested this object with the same attributes. */
1262:         if (!isset($this->_rootDSECache[$attrs_signature])) {
1263:             $this->_rootDSECache[$attrs_signature] = new Horde_Ldap_RootDse($this, $attrs);
1264:         }
1265: 
1266:         return $this->_rootDSECache[$attrs_signature];
1267:     }
1268: 
1269:     /**
1270:      * Returns a schema object
1271:      *
1272:      * @param string $dn Subschema entry dn.
1273:      *
1274:      * @return Horde_Ldap_Schema  Horde_Ldap_Schema object
1275:      * @throws Horde_Ldap_Exception
1276:      */
1277:     public function schema($dn = null)
1278:     {
1279:         /* If a schema caching object is registered, we use that to fetch a
1280:          * schema object. */
1281:         $key = 'Horde_Ldap_Schema_' . md5(serialize(array($this->_config['hostspec'], $this->_config['port'], $dn)));
1282:         if (!$this->_schema && $this->_config['cache']) {
1283:             $schema = $this->_config['cache']->get($key, $this->_config['cachettl']);
1284:             if ($schema) {
1285:                 $this->_schema = @unserialize($schema);
1286:             }
1287:         }
1288: 
1289:         /* Fetch schema, if not tried before and no cached version available.
1290:          * If we are already fetching the schema, we will skip fetching. */
1291:         if (!$this->_schema) {
1292:             /* Store a temporary error message so subsequent calls to schema()
1293:              * can detect that we are fetching the schema already. Otherwise we
1294:              * will get an infinite loop at Horde_Ldap_Schema. */
1295:             $this->_schema = new Horde_Ldap_Exception('Schema not initialized');
1296:             $this->_schema = new Horde_Ldap_Schema($this, $dn);
1297: 
1298:             /* If schema caching is active, advise the cache to store the
1299:              * schema. */
1300:             if ($this->_config['cache']) {
1301:                 $this->_config['cache']->set($key, serialize($this->_schema), $this->_config['cachettl']);
1302:             }
1303:         }
1304: 
1305:         if ($this->_schema instanceof Horde_Ldap_Exception) {
1306:             throw $this->_schema;
1307:         }
1308: 
1309:         return $this->_schema;
1310:     }
1311: 
1312:     /**
1313:      * Checks if PHP's LDAP extension is loaded.
1314:      *
1315:      * If it is not loaded, it tries to load it manually using PHP's dl().
1316:      * It knows both windows-dll and *nix-so.
1317:      *
1318:      * @throws Horde_Ldap_Exception
1319:      */
1320:     public static function checkLDAPExtension()
1321:     {
1322:         if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) {
1323:             throw new Horde_Ldap_Exception('Unable to locate PHP LDAP extension. Please install it before using the Horde_Ldap package.');
1324:         }
1325:     }
1326: 
1327:     /**
1328:      * @todo Remove this and expect all data to be UTF-8.
1329:      *
1330:      * Encodes given attributes to UTF8 if needed by schema.
1331:      *
1332:      * This function takes attributes in an array and then checks
1333:      * against the schema if they need UTF8 encoding. If that is the
1334:      * case, they will be encoded. An encoded array will be returned
1335:      * and can be used for adding or modifying.
1336:      *
1337:      * $attributes is expected to be an array with keys describing
1338:      * the attribute names and the values as the value of this attribute:
1339:      * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1340:      *
1341:      * @param array $attributes An array of attributes.
1342:      *
1343:      * @return array|Horde_Ldap_Error An array of UTF8 encoded attributes or an error.
1344:      */
1345:     public function utf8Encode($attributes)
1346:     {
1347:         return $this->utf8($attributes, 'utf8_encode');
1348:     }
1349: 
1350:     /**
1351:      * @todo Remove this and expect all data to be UTF-8.
1352:      *
1353:      * Decodes the given attribute values if needed by schema
1354:      *
1355:      * $attributes is expected to be an array with keys describing
1356:      * the attribute names and the values as the value of this attribute:
1357:      * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1358:      *
1359:      * @param array $attributes Array of attributes
1360:      *
1361:      * @access public
1362:      * @see utf8Encode()
1363:      * @return array|Horde_Ldap_Error Array with decoded attribute values or Error
1364:      */
1365:     public function utf8Decode($attributes)
1366:     {
1367:         return $this->utf8($attributes, 'utf8_decode');
1368:     }
1369: 
1370:     /**
1371:      * @todo Remove this and expect all data to be UTF-8.
1372:      *
1373:      * Encodes or decodes attribute values if needed
1374:      *
1375:      * @param array $attributes Array of attributes
1376:      * @param array $function   Function to apply to attribute values
1377:      *
1378:      * @access protected
1379:      * @return array Array of attributes with function applied to values.
1380:      */
1381:     protected function utf8($attributes, $function)
1382:     {
1383:         if (!is_array($attributes) || array_key_exists(0, $attributes)) {
1384:             throw new Horde_Ldap_Exception('Parameter $attributes is expected to be an associative array');
1385:         }
1386: 
1387:         if (!$this->_schema) {
1388:             $this->_schema = $this->schema();
1389:         }
1390: 
1391:         if (!$this->_link || !function_exists($function)) {
1392:             return $attributes;
1393:         }
1394: 
1395:         if (is_array($attributes) && count($attributes) > 0) {
1396: 
1397:             foreach ($attributes as $k => $v) {
1398: 
1399:                 if (!isset($this->_schemaAttrs[$k])) {
1400: 
1401:                     try {
1402:                         $attr = $this->_schema->get('attribute', $k);
1403:                     } catch (Exception $e) {
1404:                         continue;
1405:                     }
1406: 
1407:                     if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) {
1408:                         $encode = true;
1409:                     } else {
1410:                         $encode = false;
1411:                     }
1412:                     $this->_schemaAttrs[$k] = $encode;
1413: 
1414:                 } else {
1415:                     $encode = $this->_schemaAttrs[$k];
1416:                 }
1417: 
1418:                 if ($encode) {
1419:                     if (is_array($v)) {
1420:                         foreach ($v as $ak => $av) {
1421:                             $v[$ak] = call_user_func($function, $av);
1422:                         }
1423:                     } else {
1424:                         $v = call_user_func($function, $v);
1425:                     }
1426:                 }
1427:                 $attributes[$k] = $v;
1428:             }
1429:         }
1430:         return $attributes;
1431:     }
1432: 
1433:     /**
1434:      * Returns the LDAP link resource.
1435:      *
1436:      * It will loop attempting to re-establish the connection if the
1437:      * connection attempt fails and auto_reconnect has been turned on
1438:      * (see the _config array documentation).
1439:      *
1440:      * @return resource LDAP link.
1441:      */
1442:     public function getLink()
1443:     {
1444:         if ($this->_config['auto_reconnect']) {
1445:             while (true) {
1446:                 /* Return the link handle if we are already connected.
1447:                  * Otherwise try to reconnect. */
1448:                 if ($this->_link) {
1449:                     return $this->_link;
1450:                 }
1451:                 $this->_reconnect();
1452:             }
1453:         }
1454:         return $this->_link;
1455:     }
1456: 
1457:     /**
1458:      * Builds an LDAP search filter fragment.
1459:      *
1460:      * @param string $lhs    The attribute to test.
1461:      * @param string $op     The operator.
1462:      * @param string $rhs    The comparison value.
1463:      * @param array $params  Any additional parameters for the operator.
1464:      *
1465:      * @return string  The LDAP search fragment.
1466:      */
1467:     public static function buildClause($lhs, $op, $rhs, $params = array())
1468:     {
1469:         switch ($op) {
1470:         case 'LIKE':
1471:             if (empty($rhs)) {
1472:                 return '(' . $lhs . '=*)';
1473:             }
1474:             if (!empty($params['begin'])) {
1475:                 return sprintf('(|(%s=%s*)(%s=* %s*))', $lhs, self::quote($rhs), $lhs, self::quote($rhs));
1476:             }
1477:             if (!empty($params['approximate'])) {
1478:                 return sprintf('(%s=~%s)', $lhs, self::quote($rhs));
1479:             }
1480:             return sprintf('(%s=*%s*)', $lhs, self::quote($rhs));
1481: 
1482:         default:
1483:             return sprintf('(%s%s%s)', $lhs, $op, self::quote($rhs));
1484:         }
1485:     }
1486: 
1487: 
1488:     /**
1489:      * Escapes characters with special meaning in LDAP searches.
1490:      *
1491:      * @param string $clause  The string to escape.
1492:      *
1493:      * @return string  The escaped string.
1494:      */
1495:     public static function quote($clause)
1496:     {
1497:         return str_replace(array('\\',   '(',  ')',  '*',  "\0"),
1498:                            array('\\5c', '\(', '\)', '\*', "\\00"),
1499:                            $clause);
1500:     }
1501: 
1502:     /**
1503:      * Takes an array of DN elements and properly quotes it according to RFC
1504:      * 1485.
1505:      *
1506:      * @param array $parts  An array of tuples containing the attribute
1507:      *                      name and that attribute's value which make
1508:      *                      up the DN. Example:
1509:      *                      <code>
1510:      *                      $parts = array(0 => array('cn', 'John Smith'),
1511:      *                                     1 => array('dc', 'example'),
1512:      *                                     2 => array('dc', 'com'));
1513:      *                      </code>
1514:      *
1515:      * @return string  The properly quoted string DN.
1516:      */
1517:     public static function quoteDN($parts)
1518:     {
1519:         $dn = '';
1520:         $count = count($parts);
1521:         for ($i = 0; $i < $count; $i++) {
1522:             if ($i > 0) {
1523:                 $dn .= ',';
1524:             }
1525:             $dn .= $parts[$i][0] . '=';
1526: 
1527:             // See if we need to quote the value.
1528:             if (preg_match('/^\s|\s$|\s\s|[,+="\r\n<>#;]/', $parts[$i][1])) {
1529:                 $dn .= '"' . str_replace('"', '\\"', $parts[$i][1]) . '"';
1530:             } else {
1531:                 $dn .= $parts[$i][1];
1532:             }
1533:         }
1534: 
1535:         return $dn;
1536:     }
1537: }
1538: 
API documentation generated by ApiGen