1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Ldap; 11 12use Traversable; 13use Zend\Stdlib\ErrorHandler; 14 15class Ldap 16{ 17 const SEARCH_SCOPE_SUB = 1; 18 const SEARCH_SCOPE_ONE = 2; 19 const SEARCH_SCOPE_BASE = 3; 20 21 const ACCTNAME_FORM_DN = 1; 22 const ACCTNAME_FORM_USERNAME = 2; 23 const ACCTNAME_FORM_BACKSLASH = 3; 24 const ACCTNAME_FORM_PRINCIPAL = 4; 25 26 /** 27 * String used with ldap_connect for error handling purposes. 28 * 29 * @var string 30 */ 31 private $connectString; 32 33 /** 34 * The options used in connecting, binding, etc. 35 * 36 * @var array 37 */ 38 protected $options = null; 39 40 /** 41 * The raw LDAP extension resource. 42 * 43 * @var resource 44 */ 45 protected $resource = null; 46 47 /** 48 * FALSE if no user is bound to the LDAP resource 49 * NULL if there has been an anonymous bind 50 * username of the currently bound user 51 * 52 * @var bool|null|string 53 */ 54 protected $boundUser = false; 55 56 /** 57 * Caches the RootDse 58 * 59 * @var Node\RootDse 60 */ 61 protected $rootDse = null; 62 63 /** 64 * Caches the schema 65 * 66 * @var Node\Schema 67 */ 68 protected $schema = null; 69 70 /** 71 * Constructor. 72 * 73 * @param array|Traversable $options Options used in connecting, binding, etc. 74 * @throws Exception\LdapException 75 */ 76 public function __construct($options = array()) 77 { 78 if (!extension_loaded('ldap')) { 79 throw new Exception\LdapException(null, 'LDAP extension not loaded', 80 Exception\LdapException::LDAP_X_EXTENSION_NOT_LOADED); 81 } 82 $this->setOptions($options); 83 } 84 85 /** 86 * Destructor. 87 * 88 * @return void 89 */ 90 public function __destruct() 91 { 92 $this->disconnect(); 93 } 94 95 /** 96 * @return resource The raw LDAP extension resource. 97 */ 98 public function getResource() 99 { 100 if (!is_resource($this->resource) || $this->boundUser === false) { 101 $this->bind(); 102 } 103 104 return $this->resource; 105 } 106 107 /** 108 * Return the LDAP error number of the last LDAP command 109 * 110 * @return int 111 */ 112 public function getLastErrorCode() 113 { 114 ErrorHandler::start(E_WARNING); 115 $ret = ldap_get_option($this->resource, LDAP_OPT_ERROR_NUMBER, $err); 116 ErrorHandler::stop(); 117 if ($ret === true) { 118 if ($err <= -1 && $err >= -17) { 119 /* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error 120 * codes in OpenLDAP are negative values from -1 to -17. 121 */ 122 $err = Exception\LdapException::LDAP_SERVER_DOWN + (-$err - 1); 123 } 124 return $err; 125 } 126 127 return 0; 128 } 129 130 /** 131 * Return the LDAP error message of the last LDAP command 132 * 133 * @param int $errorCode 134 * @param array $errorMessages 135 * @return string 136 */ 137 public function getLastError(&$errorCode = null, array &$errorMessages = null) 138 { 139 $errorCode = $this->getLastErrorCode(); 140 $errorMessages = array(); 141 142 /* The various error retrieval functions can return 143 * different things so we just try to collect what we 144 * can and eliminate dupes. 145 */ 146 ErrorHandler::start(E_WARNING); 147 $estr1 = ldap_error($this->resource); 148 ErrorHandler::stop(); 149 if ($errorCode !== 0 && $estr1 === 'Success') { 150 ErrorHandler::start(E_WARNING); 151 $estr1 = ldap_err2str($errorCode); 152 ErrorHandler::stop(); 153 } 154 if (!empty($estr1)) { 155 $errorMessages[] = $estr1; 156 } 157 158 ErrorHandler::start(E_WARNING); 159 ldap_get_option($this->resource, LDAP_OPT_ERROR_STRING, $estr2); 160 ErrorHandler::stop(); 161 if (!empty($estr2) && !in_array($estr2, $errorMessages)) { 162 $errorMessages[] = $estr2; 163 } 164 165 $message = ''; 166 if ($errorCode > 0) { 167 $message = '0x' . dechex($errorCode) . ' '; 168 } 169 170 if (count($errorMessages) > 0) { 171 $message .= '(' . implode('; ', $errorMessages) . ')'; 172 } else { 173 $message .= '(no error message from LDAP)'; 174 } 175 176 return $message; 177 } 178 179 /** 180 * Get the currently bound user 181 * 182 * FALSE if no user is bound to the LDAP resource 183 * NULL if there has been an anonymous bind 184 * username of the currently bound user 185 * 186 * @return bool|null|string 187 */ 188 public function getBoundUser() 189 { 190 return $this->boundUser; 191 } 192 193 /** 194 * Sets the options used in connecting, binding, etc. 195 * 196 * Valid option keys: 197 * host 198 * port 199 * useSsl 200 * username 201 * password 202 * bindRequiresDn 203 * baseDn 204 * accountCanonicalForm 205 * accountDomainName 206 * accountDomainNameShort 207 * accountFilterFormat 208 * allowEmptyPassword 209 * useStartTls 210 * optReferrals 211 * tryUsernameSplit 212 * networkTimeout 213 * 214 * @param array|Traversable $options Options used in connecting, binding, etc. 215 * @return Ldap Provides a fluent interface 216 * @throws Exception\LdapException 217 */ 218 public function setOptions($options) 219 { 220 if ($options instanceof Traversable) { 221 $options = iterator_to_array($options); 222 } 223 224 $permittedOptions = array( 225 'host' => null, 226 'port' => 0, 227 'useSsl' => false, 228 'username' => null, 229 'password' => null, 230 'bindRequiresDn' => false, 231 'baseDn' => null, 232 'accountCanonicalForm' => null, 233 'accountDomainName' => null, 234 'accountDomainNameShort' => null, 235 'accountFilterFormat' => null, 236 'allowEmptyPassword' => false, 237 'useStartTls' => false, 238 'optReferrals' => false, 239 'tryUsernameSplit' => true, 240 'networkTimeout' => null, 241 ); 242 243 foreach ($permittedOptions as $key => $val) { 244 if (array_key_exists($key, $options)) { 245 $val = $options[$key]; 246 unset($options[$key]); 247 /* Enforce typing. This eliminates issues like Zend\Config\Reader\Ini 248 * returning '1' as a string (ZF-3163). 249 */ 250 switch ($key) { 251 case 'port': 252 case 'accountCanonicalForm': 253 case 'networkTimeout': 254 $permittedOptions[$key] = (int) $val; 255 break; 256 case 'useSsl': 257 case 'bindRequiresDn': 258 case 'allowEmptyPassword': 259 case 'useStartTls': 260 case 'optReferrals': 261 case 'tryUsernameSplit': 262 $permittedOptions[$key] = ($val === true 263 || $val === '1' 264 || strcasecmp($val, 'true') == 0); 265 break; 266 default: 267 $permittedOptions[$key] = trim($val); 268 break; 269 } 270 } 271 } 272 if (count($options) > 0) { 273 $key = key($options); 274 throw new Exception\LdapException(null, "Unknown Zend\\Ldap\\Ldap option: $key"); 275 } 276 $this->options = $permittedOptions; 277 278 return $this; 279 } 280 281 /** 282 * @return array The current options. 283 */ 284 public function getOptions() 285 { 286 return $this->options; 287 } 288 289 /** 290 * @return string The hostname of the LDAP server being used to 291 * authenticate accounts 292 */ 293 protected function getHost() 294 { 295 return $this->options['host']; 296 } 297 298 /** 299 * @return int The port of the LDAP server or 0 to indicate that no port 300 * value is set 301 */ 302 protected function getPort() 303 { 304 return $this->options['port']; 305 } 306 307 /** 308 * @return bool The default SSL / TLS encrypted transport control 309 */ 310 protected function getUseSsl() 311 { 312 return $this->options['useSsl']; 313 } 314 315 /** 316 * @return string The default acctname for binding 317 */ 318 protected function getUsername() 319 { 320 return $this->options['username']; 321 } 322 323 /** 324 * @return string The default password for binding 325 */ 326 protected function getPassword() 327 { 328 return $this->options['password']; 329 } 330 331 /** 332 * @return bool Bind requires DN 333 */ 334 protected function getBindRequiresDn() 335 { 336 return $this->options['bindRequiresDn']; 337 } 338 339 /** 340 * Gets the base DN under which objects of interest are located 341 * 342 * @return string 343 */ 344 public function getBaseDn() 345 { 346 return $this->options['baseDn']; 347 } 348 349 /** 350 * @return int Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or 351 * ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to. 352 */ 353 protected function getAccountCanonicalForm() 354 { 355 /* Account names should always be qualified with a domain. In some scenarios 356 * using non-qualified account names can lead to security vulnerabilities. If 357 * no account canonical form is specified, we guess based in what domain 358 * names have been supplied. 359 */ 360 $accountCanonicalForm = $this->options['accountCanonicalForm']; 361 if (!$accountCanonicalForm) { 362 $accountDomainName = $this->getAccountDomainName(); 363 $accountDomainNameShort = $this->getAccountDomainNameShort(); 364 if ($accountDomainNameShort) { 365 $accountCanonicalForm = self::ACCTNAME_FORM_BACKSLASH; 366 } else { 367 if ($accountDomainName) { 368 $accountCanonicalForm = self::ACCTNAME_FORM_PRINCIPAL; 369 } else { 370 $accountCanonicalForm = self::ACCTNAME_FORM_USERNAME; 371 } 372 } 373 } 374 375 return $accountCanonicalForm; 376 } 377 378 /** 379 * @return string The account domain name 380 */ 381 protected function getAccountDomainName() 382 { 383 return $this->options['accountDomainName']; 384 } 385 386 /** 387 * @return string The short account domain name 388 */ 389 protected function getAccountDomainNameShort() 390 { 391 return $this->options['accountDomainNameShort']; 392 } 393 394 /** 395 * @return string A format string for building an LDAP search filter to match 396 * an account 397 */ 398 protected function getAccountFilterFormat() 399 { 400 return $this->options['accountFilterFormat']; 401 } 402 403 /** 404 * @return bool Allow empty passwords 405 */ 406 protected function getAllowEmptyPassword() 407 { 408 return $this->options['allowEmptyPassword']; 409 } 410 411 /** 412 * @return bool The default SSL / TLS encrypted transport control 413 */ 414 protected function getUseStartTls() 415 { 416 return $this->options['useStartTls']; 417 } 418 419 /** 420 * @return bool Opt. Referrals 421 */ 422 protected function getOptReferrals() 423 { 424 return $this->options['optReferrals']; 425 } 426 427 /** 428 * @return bool Try splitting the username into username and domain 429 */ 430 protected function getTryUsernameSplit() 431 { 432 return $this->options['tryUsernameSplit']; 433 } 434 435 /** 436 * @return int The value for network timeout when connect to the LDAP server. 437 */ 438 protected function getNetworkTimeout() 439 { 440 return $this->options['networkTimeout']; 441 } 442 443 /** 444 * @param string $acctname 445 * @return string The LDAP search filter for matching directory accounts 446 */ 447 protected function getAccountFilter($acctname) 448 { 449 $dname = ''; 450 $aname = ''; 451 $this->splitName($acctname, $dname, $aname); 452 $accountFilterFormat = $this->getAccountFilterFormat(); 453 $aname = Filter\AbstractFilter::escapeValue($aname); 454 if ($accountFilterFormat) { 455 return sprintf($accountFilterFormat, $aname); 456 } 457 if (!$this->getBindRequiresDn()) { 458 // is there a better way to detect this? 459 return sprintf("(&(objectClass=user)(sAMAccountName=%s))", $aname); 460 } 461 462 return sprintf("(&(objectClass=posixAccount)(uid=%s))", $aname); 463 } 464 465 /** 466 * @param string $name The name to split 467 * @param string $dname The resulting domain name (this is an out parameter) 468 * @param string $aname The resulting account name (this is an out parameter) 469 * @return void 470 */ 471 protected function splitName($name, &$dname, &$aname) 472 { 473 $dname = null; 474 $aname = $name; 475 476 if (!$this->getTryUsernameSplit()) { 477 return; 478 } 479 480 $pos = strpos($name, '@'); 481 if ($pos) { 482 $dname = substr($name, $pos + 1); 483 $aname = substr($name, 0, $pos); 484 } else { 485 $pos = strpos($name, '\\'); 486 if ($pos) { 487 $dname = substr($name, 0, $pos); 488 $aname = substr($name, $pos + 1); 489 } 490 } 491 } 492 493 /** 494 * @param string $acctname The name of the account 495 * @return string The DN of the specified account 496 * @throws Exception\LdapException 497 */ 498 protected function getAccountDn($acctname) 499 { 500 if (Dn::checkDn($acctname)) { 501 return $acctname; 502 } 503 $acctname = $this->getCanonicalAccountName($acctname, self::ACCTNAME_FORM_USERNAME); 504 $acct = $this->getAccount($acctname, array('dn')); 505 506 return $acct['dn']; 507 } 508 509 /** 510 * @param string $dname The domain name to check 511 * @return bool 512 */ 513 protected function isPossibleAuthority($dname) 514 { 515 if ($dname === null) { 516 return true; 517 } 518 $accountDomainName = $this->getAccountDomainName(); 519 $accountDomainNameShort = $this->getAccountDomainNameShort(); 520 if ($accountDomainName === null && $accountDomainNameShort === null) { 521 return true; 522 } 523 if (strcasecmp($dname, $accountDomainName) == 0) { 524 return true; 525 } 526 if (strcasecmp($dname, $accountDomainNameShort) == 0) { 527 return true; 528 } 529 530 return false; 531 } 532 533 /** 534 * @param string $acctname The name to canonicalize 535 * @param int $form The desired form of canonicalization 536 * @return string The canonicalized name in the desired form 537 * @throws Exception\LdapException 538 */ 539 public function getCanonicalAccountName($acctname, $form = 0) 540 { 541 $dname = ''; 542 $uname = ''; 543 544 $this->splitName($acctname, $dname, $uname); 545 546 if (!$this->isPossibleAuthority($dname)) { 547 throw new Exception\LdapException(null, 548 "Binding domain is not an authority for user: $acctname", 549 Exception\LdapException::LDAP_X_DOMAIN_MISMATCH); 550 } 551 552 if (!$uname) { 553 throw new Exception\LdapException(null, "Invalid account name syntax: $acctname"); 554 } 555 556 if (function_exists('mb_strtolower')) { 557 $uname = mb_strtolower($uname, 'UTF-8'); 558 } else { 559 $uname = strtolower($uname); 560 } 561 562 if ($form === 0) { 563 $form = $this->getAccountCanonicalForm(); 564 } 565 566 switch ($form) { 567 case self::ACCTNAME_FORM_DN: 568 return $this->getAccountDn($acctname); 569 case self::ACCTNAME_FORM_USERNAME: 570 return $uname; 571 case self::ACCTNAME_FORM_BACKSLASH: 572 $accountDomainNameShort = $this->getAccountDomainNameShort(); 573 if (!$accountDomainNameShort) { 574 throw new Exception\LdapException(null, 'Option required: accountDomainNameShort'); 575 } 576 return "$accountDomainNameShort\\$uname"; 577 case self::ACCTNAME_FORM_PRINCIPAL: 578 $accountDomainName = $this->getAccountDomainName(); 579 if (!$accountDomainName) { 580 throw new Exception\LdapException(null, 'Option required: accountDomainName'); 581 } 582 return "$uname@$accountDomainName"; 583 default: 584 throw new Exception\LdapException(null, "Unknown canonical name form: $form"); 585 } 586 } 587 588 /** 589 * @param string $acctname 590 * @param array $attrs An array of names of desired attributes 591 * @return array An array of the attributes representing the account 592 * @throws Exception\LdapException 593 */ 594 protected function getAccount($acctname, array $attrs = null) 595 { 596 $baseDn = $this->getBaseDn(); 597 if (!$baseDn) { 598 throw new Exception\LdapException(null, 'Base DN not set'); 599 } 600 601 $accountFilter = $this->getAccountFilter($acctname); 602 if (!$accountFilter) { 603 throw new Exception\LdapException(null, 'Invalid account filter'); 604 } 605 606 if (!is_resource($this->getResource())) { 607 $this->bind(); 608 } 609 610 $accounts = $this->search($accountFilter, $baseDn, self::SEARCH_SCOPE_SUB, $attrs); 611 $count = $accounts->count(); 612 if ($count === 1) { 613 $acct = $accounts->getFirst(); 614 $accounts->close(); 615 616 return $acct; 617 } else { 618 if ($count === 0) { 619 $code = Exception\LdapException::LDAP_NO_SUCH_OBJECT; 620 $str = "No object found for: $accountFilter"; 621 } else { 622 $code = Exception\LdapException::LDAP_OPERATIONS_ERROR; 623 $str = "Unexpected result count ($count) for: $accountFilter"; 624 } 625 } 626 $accounts->close(); 627 628 throw new Exception\LdapException($this, $str, $code); 629 } 630 631 /** 632 * @return Ldap Provides a fluent interface 633 */ 634 public function disconnect() 635 { 636 if (is_resource($this->resource)) { 637 ErrorHandler::start(E_WARNING); 638 ldap_unbind($this->resource); 639 ErrorHandler::stop(); 640 } 641 $this->resource = null; 642 $this->boundUser = false; 643 644 return $this; 645 } 646 647 /** 648 * To connect using SSL it seems the client tries to verify the server 649 * certificate by default. One way to disable this behavior is to set 650 * 'TLS_REQCERT never' in OpenLDAP's ldap.conf and restarting Apache. Or, 651 * if you really care about the server's cert you can put a cert on the 652 * web server. 653 * 654 * @param string $host The hostname of the LDAP server to connect to 655 * @param int $port The port number of the LDAP server to connect to 656 * @param bool $useSsl Use SSL 657 * @param bool $useStartTls Use STARTTLS 658 * @param int $networkTimeout The value for network timeout when connect to the LDAP server. 659 * @return Ldap Provides a fluent interface 660 * @throws Exception\LdapException 661 */ 662 public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null, $networkTimeout = null) 663 { 664 if ($host === null) { 665 $host = $this->getHost(); 666 } 667 if ($port === null) { 668 $port = $this->getPort(); 669 } else { 670 $port = (int) $port; 671 } 672 if ($useSsl === null) { 673 $useSsl = $this->getUseSsl(); 674 } else { 675 $useSsl = (bool) $useSsl; 676 } 677 if ($useStartTls === null) { 678 $useStartTls = $this->getUseStartTls(); 679 } else { 680 $useStartTls = (bool) $useStartTls; 681 } 682 if ($networkTimeout === null) { 683 $networkTimeout = $this->getNetworkTimeout(); 684 } else { 685 $networkTimeout = (int) $networkTimeout; 686 } 687 688 if (!$host) { 689 throw new Exception\LdapException(null, 'A host parameter is required'); 690 } 691 692 $useUri = false; 693 /* Because ldap_connect doesn't really try to connect, any connect error 694 * will actually occur during the ldap_bind call. Therefore, we save the 695 * connect string here for reporting it in error handling in bind(). 696 */ 697 $hosts = array(); 698 if (preg_match_all('~ldap(?:i|s)?://~', $host, $hosts, PREG_SET_ORDER) > 0) { 699 $this->connectString = $host; 700 $useUri = true; 701 $useSsl = false; 702 } else { 703 if ($useSsl) { 704 $this->connectString = 'ldaps://' . $host; 705 $useUri = true; 706 } else { 707 $this->connectString = 'ldap://' . $host; 708 } 709 if ($port) { 710 $this->connectString .= ':' . $port; 711 } 712 } 713 714 $this->disconnect(); 715 716 717 /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just 718 * use the old form. 719 */ 720 ErrorHandler::start(); 721 $resource = ($useUri) ? ldap_connect($this->connectString) : ldap_connect($host, $port); 722 ErrorHandler::stop(); 723 724 if (is_resource($resource) === true) { 725 $this->resource = $resource; 726 $this->boundUser = false; 727 728 $optReferrals = ($this->getOptReferrals()) ? 1 : 0; 729 ErrorHandler::start(E_WARNING); 730 if (ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) 731 && ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals) 732 ) { 733 if ($networkTimeout) { 734 ldap_set_option($resource, LDAP_OPT_NETWORK_TIMEOUT, $networkTimeout); 735 } 736 if ($useSsl || !$useStartTls || ldap_start_tls($resource)) { 737 ErrorHandler::stop(); 738 return $this; 739 } 740 } 741 ErrorHandler::stop(); 742 743 $zle = new Exception\LdapException($this, "$host:$port"); 744 $this->disconnect(); 745 throw $zle; 746 } 747 748 throw new Exception\LdapException(null, "Failed to connect to LDAP server: $host:$port"); 749 } 750 751 /** 752 * @param string $username The username for authenticating the bind 753 * @param string $password The password for authenticating the bind 754 * @return Ldap Provides a fluent interface 755 * @throws Exception\LdapException 756 */ 757 public function bind($username = null, $password = null) 758 { 759 $moreCreds = true; 760 761 // Security check: remove null bytes in password 762 // @see https://net.educause.edu/ir/library/pdf/csd4875.pdf 763 $password = str_replace("\0", '', $password); 764 765 if ($username === null) { 766 $username = $this->getUsername(); 767 $password = $this->getPassword(); 768 $moreCreds = false; 769 } 770 771 if (empty($username)) { 772 /* Perform anonymous bind 773 */ 774 $username = null; 775 $password = null; 776 } else { 777 /* Check to make sure the username is in DN form. 778 */ 779 if (!Dn::checkDn($username)) { 780 if ($this->getBindRequiresDn()) { 781 /* moreCreds stops an infinite loop if getUsername does not 782 * return a DN and the bind requires it 783 */ 784 if ($moreCreds) { 785 try { 786 $username = $this->getAccountDn($username); 787 } catch (Exception\LdapException $zle) { 788 switch ($zle->getCode()) { 789 case Exception\LdapException::LDAP_NO_SUCH_OBJECT: 790 case Exception\LdapException::LDAP_X_DOMAIN_MISMATCH: 791 case Exception\LdapException::LDAP_X_EXTENSION_NOT_LOADED: 792 throw $zle; 793 } 794 throw new Exception\LdapException(null, 795 'Failed to retrieve DN for account: ' . $username . 796 ' [' . $zle->getMessage() . ']', 797 Exception\LdapException::LDAP_OPERATIONS_ERROR); 798 } 799 } else { 800 throw new Exception\LdapException(null, 'Binding requires username in DN form'); 801 } 802 } else { 803 $username = $this->getCanonicalAccountName( 804 $username, 805 $this->getAccountCanonicalForm() 806 ); 807 } 808 } 809 } 810 811 if (!is_resource($this->resource)) { 812 $this->connect(); 813 } 814 815 if ($username !== null && $password === '' && $this->getAllowEmptyPassword() !== true) { 816 $zle = new Exception\LdapException(null, 817 'Empty password not allowed - see allowEmptyPassword option.'); 818 } else { 819 ErrorHandler::start(E_WARNING); 820 $bind = ldap_bind($this->resource, $username, $password); 821 ErrorHandler::stop(); 822 if ($bind) { 823 $this->boundUser = $username; 824 return $this; 825 } 826 827 $message = ($username === null) ? $this->connectString : $username; 828 switch ($this->getLastErrorCode()) { 829 case Exception\LdapException::LDAP_SERVER_DOWN: 830 /* If the error is related to establishing a connection rather than binding, 831 * the connect string is more informative than the username. 832 */ 833 $message = $this->connectString; 834 } 835 836 $zle = new Exception\LdapException($this, $message); 837 } 838 $this->disconnect(); 839 840 throw $zle; 841 } 842 843 /** 844 * A global LDAP search routine for finding information. 845 * 846 * Options can be either passed as single parameters according to the 847 * method signature or as an array with one or more of the following keys 848 * - filter 849 * - baseDn 850 * - scope 851 * - attributes 852 * - sort 853 * - collectionClass 854 * - sizelimit 855 * - timelimit 856 * 857 * @param string|Filter\AbstractFilter|array $filter 858 * @param string|Dn|null $basedn 859 * @param int $scope 860 * @param array $attributes 861 * @param string|null $sort 862 * @param string|null $collectionClass 863 * @param int $sizelimit 864 * @param int $timelimit 865 * @return Collection 866 * @throws Exception\LdapException 867 */ 868 public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, array $attributes = array(), 869 $sort = null, $collectionClass = null, $sizelimit = 0, $timelimit = 0 870 ) { 871 if (is_array($filter)) { 872 $options = array_change_key_case($filter, CASE_LOWER); 873 foreach ($options as $key => $value) { 874 switch ($key) { 875 case 'filter': 876 case 'basedn': 877 case 'scope': 878 case 'sort': 879 $$key = $value; 880 break; 881 case 'attributes': 882 if (is_array($value)) { 883 $attributes = $value; 884 } 885 break; 886 case 'collectionclass': 887 $collectionClass = $value; 888 break; 889 case 'sizelimit': 890 case 'timelimit': 891 $$key = (int) $value; 892 break; 893 } 894 } 895 } 896 897 if ($basedn === null) { 898 $basedn = $this->getBaseDn(); 899 } elseif ($basedn instanceof Dn) { 900 $basedn = $basedn->toString(); 901 } 902 903 if ($filter instanceof Filter\AbstractFilter) { 904 $filter = $filter->toString(); 905 } 906 907 $resource = $this->getResource(); 908 ErrorHandler::start(E_WARNING); 909 switch ($scope) { 910 case self::SEARCH_SCOPE_ONE: 911 $search = ldap_list($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); 912 break; 913 case self::SEARCH_SCOPE_BASE: 914 $search = ldap_read($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); 915 break; 916 case self::SEARCH_SCOPE_SUB: 917 default: 918 $search = ldap_search($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); 919 break; 920 } 921 ErrorHandler::stop(); 922 923 if ($search === false) { 924 throw new Exception\LdapException($this, 'searching: ' . $filter); 925 } 926 if ($sort !== null && is_string($sort)) { 927 ErrorHandler::start(E_WARNING); 928 $isSorted = ldap_sort($resource, $search, $sort); 929 ErrorHandler::stop(); 930 if ($isSorted === false) { 931 throw new Exception\LdapException($this, 'sorting: ' . $sort); 932 } 933 } 934 935 $iterator = new Collection\DefaultIterator($this, $search); 936 937 return $this->createCollection($iterator, $collectionClass); 938 } 939 940 /** 941 * Extension point for collection creation 942 * 943 * @param Collection\DefaultIterator $iterator 944 * @param string|null $collectionClass 945 * @return Collection 946 * @throws Exception\LdapException 947 */ 948 protected function createCollection(Collection\DefaultIterator $iterator, $collectionClass) 949 { 950 if ($collectionClass === null) { 951 return new Collection($iterator); 952 } else { 953 $collectionClass = (string) $collectionClass; 954 if (!class_exists($collectionClass)) { 955 throw new Exception\LdapException(null, 956 "Class '$collectionClass' can not be found"); 957 } 958 if (!is_subclass_of($collectionClass, 'Zend\Ldap\Collection')) { 959 throw new Exception\LdapException(null, 960 "Class '$collectionClass' must subclass 'Zend\\Ldap\\Collection'"); 961 } 962 963 return new $collectionClass($iterator); 964 } 965 } 966 967 /** 968 * Count items found by given filter. 969 * 970 * @param string|Filter\AbstractFilter $filter 971 * @param string|Dn|null $basedn 972 * @param int $scope 973 * @return int 974 * @throws Exception\LdapException 975 */ 976 public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB) 977 { 978 try { 979 $result = $this->search($filter, $basedn, $scope, array('dn'), null); 980 } catch (Exception\LdapException $e) { 981 if ($e->getCode() === Exception\LdapException::LDAP_NO_SUCH_OBJECT) { 982 return 0; 983 } 984 throw $e; 985 } 986 987 return $result->count(); 988 } 989 990 /** 991 * Count children for a given DN. 992 * 993 * @param string|Dn $dn 994 * @return int 995 * @throws Exception\LdapException 996 */ 997 public function countChildren($dn) 998 { 999 return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE); 1000 } 1001 1002 /** 1003 * Check if a given DN exists. 1004 * 1005 * @param string|Dn $dn 1006 * @return bool 1007 * @throws Exception\LdapException 1008 */ 1009 public function exists($dn) 1010 { 1011 return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE) == 1); 1012 } 1013 1014 /** 1015 * Search LDAP registry for entries matching filter and optional attributes 1016 * 1017 * Options can be either passed as single parameters according to the 1018 * method signature or as an array with one or more of the following keys 1019 * - filter 1020 * - baseDn 1021 * - scope 1022 * - attributes 1023 * - sort 1024 * - reverseSort 1025 * - sizelimit 1026 * - timelimit 1027 * 1028 * @param string|Filter\AbstractFilter|array $filter 1029 * @param string|Dn|null $basedn 1030 * @param int $scope 1031 * @param array $attributes 1032 * @param string|null $sort 1033 * @param bool $reverseSort 1034 * @param int $sizelimit 1035 * @param int $timelimit 1036 * @return array 1037 * @throws Exception\LdapException 1038 */ 1039 public function searchEntries($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, 1040 array $attributes = array(), $sort = null, $reverseSort = false, $sizelimit = 0, 1041 $timelimit = 0) 1042 { 1043 if (is_array($filter)) { 1044 $filter = array_change_key_case($filter, CASE_LOWER); 1045 if (isset($filter['collectionclass'])) { 1046 unset($filter['collectionclass']); 1047 } 1048 if (isset($filter['reversesort'])) { 1049 $reverseSort = $filter['reversesort']; 1050 unset($filter['reversesort']); 1051 } 1052 } 1053 $result = $this->search($filter, $basedn, $scope, $attributes, $sort, null, $sizelimit, $timelimit); 1054 $items = $result->toArray(); 1055 if ((bool) $reverseSort === true) { 1056 $items = array_reverse($items, false); 1057 } 1058 1059 return $items; 1060 } 1061 1062 /** 1063 * Get LDAP entry by DN 1064 * 1065 * @param string|Dn $dn 1066 * @param array $attributes 1067 * @param bool $throwOnNotFound 1068 * @return array 1069 * @throws null|Exception\LdapException 1070 */ 1071 public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false) 1072 { 1073 try { 1074 $result = $this->search( 1075 "(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE, 1076 $attributes, null 1077 ); 1078 1079 return $result->getFirst(); 1080 } catch (Exception\LdapException $e) { 1081 if ($throwOnNotFound !== false) { 1082 throw $e; 1083 } 1084 } 1085 1086 return; 1087 } 1088 1089 /** 1090 * Prepares an ldap data entry array for insert/update operation 1091 * 1092 * @param array $entry 1093 * @throws Exception\InvalidArgumentException 1094 * @return void 1095 */ 1096 public static function prepareLdapEntryArray(array &$entry) 1097 { 1098 if (array_key_exists('dn', $entry)) { 1099 unset($entry['dn']); 1100 } 1101 foreach ($entry as $key => $value) { 1102 if (is_array($value)) { 1103 foreach ($value as $i => $v) { 1104 if ($v === null) { 1105 unset($value[$i]); 1106 } elseif (!is_scalar($v)) { 1107 throw new Exception\InvalidArgumentException('Only scalar values allowed in LDAP data'); 1108 } else { 1109 $v = (string) $v; 1110 if (strlen($v) == 0) { 1111 unset($value[$i]); 1112 } else { 1113 $value[$i] = $v; 1114 } 1115 } 1116 } 1117 $entry[$key] = array_values($value); 1118 } else { 1119 if ($value === null) { 1120 $entry[$key] = array(); 1121 } elseif (!is_scalar($value)) { 1122 throw new Exception\InvalidArgumentException('Only scalar values allowed in LDAP data'); 1123 } else { 1124 $value = (string) $value; 1125 if (strlen($value) == 0) { 1126 $entry[$key] = array(); 1127 } else { 1128 $entry[$key] = array($value); 1129 } 1130 } 1131 } 1132 } 1133 $entry = array_change_key_case($entry, CASE_LOWER); 1134 } 1135 1136 /** 1137 * Add new information to the LDAP repository 1138 * 1139 * @param string|Dn $dn 1140 * @param array $entry 1141 * @return Ldap Provides a fluid interface 1142 * @throws Exception\LdapException 1143 */ 1144 public function add($dn, array $entry) 1145 { 1146 if (!($dn instanceof Dn)) { 1147 $dn = Dn::factory($dn, null); 1148 } 1149 static::prepareLdapEntryArray($entry); 1150 foreach ($entry as $key => $value) { 1151 if (is_array($value) && count($value) === 0) { 1152 unset($entry[$key]); 1153 } 1154 } 1155 1156 $rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER); 1157 foreach ($rdnParts as $key => $value) { 1158 $value = Dn::unescapeValue($value); 1159 if (!array_key_exists($key, $entry)) { 1160 $entry[$key] = array($value); 1161 } elseif (!in_array($value, $entry[$key])) { 1162 $entry[$key] = array_merge(array($value), $entry[$key]); 1163 } 1164 } 1165 $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', 1166 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); 1167 foreach ($adAttributes as $attr) { 1168 if (array_key_exists($attr, $entry)) { 1169 unset($entry[$attr]); 1170 } 1171 } 1172 1173 $resource = $this->getResource(); 1174 ErrorHandler::start(E_WARNING); 1175 $isAdded = ldap_add($resource, $dn->toString(), $entry); 1176 ErrorHandler::stop(); 1177 if ($isAdded === false) { 1178 throw new Exception\LdapException($this, 'adding: ' . $dn->toString()); 1179 } 1180 1181 return $this; 1182 } 1183 1184 /** 1185 * Update LDAP registry 1186 * 1187 * @param string|Dn $dn 1188 * @param array $entry 1189 * @return Ldap Provides a fluid interface 1190 * @throws Exception\LdapException 1191 */ 1192 public function update($dn, array $entry) 1193 { 1194 if (!($dn instanceof Dn)) { 1195 $dn = Dn::factory($dn, null); 1196 } 1197 static::prepareLdapEntryArray($entry); 1198 1199 $rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER); 1200 foreach ($rdnParts as $key => $value) { 1201 $value = Dn::unescapeValue($value); 1202 if (array_key_exists($key, $entry) && !in_array($value, $entry[$key])) { 1203 $entry[$key] = array_merge(array($value), $entry[$key]); 1204 } 1205 } 1206 $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', 1207 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); 1208 foreach ($adAttributes as $attr) { 1209 if (array_key_exists($attr, $entry)) { 1210 unset($entry[$attr]); 1211 } 1212 } 1213 1214 if (count($entry) > 0) { 1215 $resource = $this->getResource(); 1216 ErrorHandler::start(E_WARNING); 1217 $isModified = ldap_modify($resource, $dn->toString(), $entry); 1218 ErrorHandler::stop(); 1219 if ($isModified === false) { 1220 throw new Exception\LdapException($this, 'updating: ' . $dn->toString()); 1221 } 1222 } 1223 1224 return $this; 1225 } 1226 1227 /** 1228 * Save entry to LDAP registry. 1229 * 1230 * Internally decides if entry will be updated to added by calling 1231 * {@link exists()}. 1232 * 1233 * @param string|Dn $dn 1234 * @param array $entry 1235 * @return Ldap Provides a fluid interface 1236 * @throws Exception\LdapException 1237 */ 1238 public function save($dn, array $entry) 1239 { 1240 if ($dn instanceof Dn) { 1241 $dn = $dn->toString(); 1242 } 1243 if ($this->exists($dn)) { 1244 $this->update($dn, $entry); 1245 } else { 1246 $this->add($dn, $entry); 1247 } 1248 1249 return $this; 1250 } 1251 1252 /** 1253 * Delete an LDAP entry 1254 * 1255 * @param string|Dn $dn 1256 * @param bool $recursively 1257 * @return Ldap Provides a fluid interface 1258 * @throws Exception\LdapException 1259 */ 1260 public function delete($dn, $recursively = false) 1261 { 1262 if ($dn instanceof Dn) { 1263 $dn = $dn->toString(); 1264 } 1265 if ($recursively === true) { 1266 if ($this->countChildren($dn) > 0) { 1267 $children = $this->getChildrenDns($dn); 1268 foreach ($children as $c) { 1269 $this->delete($c, true); 1270 } 1271 } 1272 } 1273 1274 $resource = $this->getResource(); 1275 ErrorHandler::start(E_WARNING); 1276 $isDeleted = ldap_delete($resource, $dn); 1277 ErrorHandler::stop(); 1278 if ($isDeleted === false) { 1279 throw new Exception\LdapException($this, 'deleting: ' . $dn); 1280 } 1281 1282 return $this; 1283 } 1284 1285 /** 1286 * Retrieve the immediate children DNs of the given $parentDn 1287 * 1288 * This method is used in recursive methods like {@see delete()} 1289 * or {@see copy()} 1290 * 1291 * @param string|Dn $parentDn 1292 * @throws Exception\LdapException 1293 * @return array of DNs 1294 */ 1295 protected function getChildrenDns($parentDn) 1296 { 1297 if ($parentDn instanceof Dn) { 1298 $parentDn = $parentDn->toString(); 1299 } 1300 $children = array(); 1301 1302 $resource = $this->getResource(); 1303 ErrorHandler::start(E_WARNING); 1304 $search = ldap_list($resource, $parentDn, '(objectClass=*)', array('dn')); 1305 for ( 1306 $entry = ldap_first_entry($resource, $search); 1307 $entry !== false; 1308 $entry = ldap_next_entry($resource, $entry) 1309 ) { 1310 $childDn = ldap_get_dn($resource, $entry); 1311 if ($childDn === false) { 1312 ErrorHandler::stop(); 1313 throw new Exception\LdapException($this, 'getting dn'); 1314 } 1315 $children[] = $childDn; 1316 } 1317 ldap_free_result($search); 1318 ErrorHandler::stop(); 1319 1320 return $children; 1321 } 1322 1323 /** 1324 * Moves a LDAP entry from one DN to another subtree. 1325 * 1326 * @param string|Dn $from 1327 * @param string|Dn $to 1328 * @param bool $recursively 1329 * @param bool $alwaysEmulate 1330 * @return Ldap Provides a fluid interface 1331 * @throws Exception\LdapException 1332 */ 1333 public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false) 1334 { 1335 if ($from instanceof Dn) { 1336 $orgDnParts = $from->toArray(); 1337 } else { 1338 $orgDnParts = Dn::explodeDn($from); 1339 } 1340 1341 if ($to instanceof Dn) { 1342 $newParentDnParts = $to->toArray(); 1343 } else { 1344 $newParentDnParts = Dn::explodeDn($to); 1345 } 1346 1347 $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts); 1348 $newDn = Dn::fromArray($newDnParts); 1349 1350 return $this->rename($from, $newDn, $recursively, $alwaysEmulate); 1351 } 1352 1353 /** 1354 * Moves a LDAP entry from one DN to another DN. 1355 * 1356 * This is an alias for {@link rename()} 1357 * 1358 * @param string|Dn $from 1359 * @param string|Dn $to 1360 * @param bool $recursively 1361 * @param bool $alwaysEmulate 1362 * @return Ldap Provides a fluid interface 1363 * @throws Exception\LdapException 1364 */ 1365 public function move($from, $to, $recursively = false, $alwaysEmulate = false) 1366 { 1367 return $this->rename($from, $to, $recursively, $alwaysEmulate); 1368 } 1369 1370 /** 1371 * Renames a LDAP entry from one DN to another DN. 1372 * 1373 * This method implicitly moves the entry to another location within the tree. 1374 * 1375 * @param string|Dn $from 1376 * @param string|Dn $to 1377 * @param bool $recursively 1378 * @param bool $alwaysEmulate 1379 * @return Ldap Provides a fluid interface 1380 * @throws Exception\LdapException 1381 */ 1382 public function rename($from, $to, $recursively = false, $alwaysEmulate = false) 1383 { 1384 $emulate = (bool) $alwaysEmulate; 1385 if (!function_exists('ldap_rename')) { 1386 $emulate = true; 1387 } elseif ($recursively) { 1388 $emulate = true; 1389 } 1390 1391 if ($emulate === false) { 1392 if ($from instanceof Dn) { 1393 $from = $from->toString(); 1394 } 1395 1396 if ($to instanceof Dn) { 1397 $newDnParts = $to->toArray(); 1398 } else { 1399 $newDnParts = Dn::explodeDn($to); 1400 } 1401 1402 $newRdn = Dn::implodeRdn(array_shift($newDnParts)); 1403 $newParent = Dn::implodeDn($newDnParts); 1404 1405 $resource = $this->getResource(); 1406 ErrorHandler::start(E_WARNING); 1407 $isOK = ldap_rename($resource, $from, $newRdn, $newParent, true); 1408 ErrorHandler::stop(); 1409 if ($isOK === false) { 1410 throw new Exception\LdapException($this, 'renaming ' . $from . ' to ' . $to); 1411 } elseif (!$this->exists($to)) { 1412 $emulate = true; 1413 } 1414 } 1415 if ($emulate) { 1416 $this->copy($from, $to, $recursively); 1417 $this->delete($from, $recursively); 1418 } 1419 1420 return $this; 1421 } 1422 1423 /** 1424 * Copies a LDAP entry from one DN to another subtree. 1425 * 1426 * @param string|Dn $from 1427 * @param string|Dn $to 1428 * @param bool $recursively 1429 * @return Ldap Provides a fluid interface 1430 * @throws Exception\LdapException 1431 */ 1432 public function copyToSubtree($from, $to, $recursively = false) 1433 { 1434 if ($from instanceof Dn) { 1435 $orgDnParts = $from->toArray(); 1436 } else { 1437 $orgDnParts = Dn::explodeDn($from); 1438 } 1439 1440 if ($to instanceof Dn) { 1441 $newParentDnParts = $to->toArray(); 1442 } else { 1443 $newParentDnParts = Dn::explodeDn($to); 1444 } 1445 1446 $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts); 1447 $newDn = Dn::fromArray($newDnParts); 1448 1449 return $this->copy($from, $newDn, $recursively); 1450 } 1451 1452 /** 1453 * Copies a LDAP entry from one DN to another DN. 1454 * 1455 * @param string|Dn $from 1456 * @param string|Dn $to 1457 * @param bool $recursively 1458 * @return Ldap Provides a fluid interface 1459 * @throws Exception\LdapException 1460 */ 1461 public function copy($from, $to, $recursively = false) 1462 { 1463 $entry = $this->getEntry($from, array(), true); 1464 1465 if ($to instanceof Dn) { 1466 $toDnParts = $to->toArray(); 1467 } else { 1468 $toDnParts = Dn::explodeDn($to); 1469 } 1470 $this->add($to, $entry); 1471 1472 if ($recursively === true && $this->countChildren($from) > 0) { 1473 $children = $this->getChildrenDns($from); 1474 foreach ($children as $c) { 1475 $cDnParts = Dn::explodeDn($c); 1476 $newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts); 1477 $newChild = Dn::implodeDn($newChildParts); 1478 $this->copy($c, $newChild, true); 1479 } 1480 } 1481 1482 return $this; 1483 } 1484 1485 /** 1486 * Returns the specified DN as a Zend\Ldap\Node 1487 * 1488 * @param string|Dn $dn 1489 * @return Node|null 1490 * @throws Exception\LdapException 1491 */ 1492 public function getNode($dn) 1493 { 1494 return Node::fromLdap($dn, $this); 1495 } 1496 1497 /** 1498 * Returns the base node as a Zend\Ldap\Node 1499 * 1500 * @return Node 1501 * @throws Exception\LdapException 1502 */ 1503 public function getBaseNode() 1504 { 1505 return $this->getNode($this->getBaseDn(), $this); 1506 } 1507 1508 /** 1509 * Returns the RootDse 1510 * 1511 * @return Node\RootDse 1512 * @throws Exception\LdapException 1513 */ 1514 public function getRootDse() 1515 { 1516 if ($this->rootDse === null) { 1517 $this->rootDse = Node\RootDse::create($this); 1518 } 1519 1520 return $this->rootDse; 1521 } 1522 1523 /** 1524 * Returns the schema 1525 * 1526 * @return Node\Schema 1527 * @throws Exception\LdapException 1528 */ 1529 public function getSchema() 1530 { 1531 if ($this->schema === null) { 1532 $this->schema = Node\Schema::create($this); 1533 } 1534 1535 return $this->schema; 1536 } 1537} 1538