Added the zend framework 2 library, the path is specified in line no.26 in zend_modul...
[openemr.git] / interface / modules / zend_modules / library / Zend / Ldap / Ldap.php
blob6deafa831915fab468ce8aa5f5f304ebf460fe45
1 <?php
2 /**
3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
8 */
10 namespace Zend\Ldap;
12 use Traversable;
13 use Zend\Stdlib\ErrorHandler;
15 class Ldap
17 const SEARCH_SCOPE_SUB = 1;
18 const SEARCH_SCOPE_ONE = 2;
19 const SEARCH_SCOPE_BASE = 3;
21 const ACCTNAME_FORM_DN = 1;
22 const ACCTNAME_FORM_USERNAME = 2;
23 const ACCTNAME_FORM_BACKSLASH = 3;
24 const ACCTNAME_FORM_PRINCIPAL = 4;
26 /**
27 * String used with ldap_connect for error handling purposes.
29 * @var string
31 private $connectString;
33 /**
34 * The options used in connecting, binding, etc.
36 * @var array
38 protected $options = null;
40 /**
41 * The raw LDAP extension resource.
43 * @var resource
45 protected $resource = null;
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
52 * @var bool|null|string
54 protected $boundUser = false;
56 /**
57 * Caches the RootDse
59 * @var Node\RootDse
61 protected $rootDse = null;
63 /**
64 * Caches the schema
66 * @var Node\Schema
68 protected $schema = null;
70 /**
71 * Constructor.
73 * @param array|Traversable $options Options used in connecting, binding, etc.
74 * @throws Exception\LdapException
76 public function __construct($options = array())
78 if (!extension_loaded('ldap')) {
79 throw new Exception\LdapException(null, 'LDAP extension not loaded',
80 Exception\LdapException::LDAP_X_EXTENSION_NOT_LOADED);
82 $this->setOptions($options);
85 /**
86 * Destructor.
88 * @return void
90 public function __destruct()
92 $this->disconnect();
95 /**
96 * @return resource The raw LDAP extension resource.
98 public function getResource()
100 if (!is_resource($this->resource) || $this->boundUser === false) {
101 $this->bind();
104 return $this->resource;
108 * Return the LDAP error number of the last LDAP command
110 * @return int
112 public function getLastErrorCode()
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.
122 $err = Exception\LdapException::LDAP_SERVER_DOWN + (-$err - 1);
124 return $err;
127 return 0;
131 * Return the LDAP error message of the last LDAP command
133 * @param int $errorCode
134 * @param array $errorMessages
135 * @return string
137 public function getLastError(&$errorCode = null, array &$errorMessages = null)
139 $errorCode = $this->getLastErrorCode();
140 $errorMessages = array();
142 /* The various error retrieval functions can return
143 * different things so we just try to collect what we
144 * can and eliminate dupes.
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();
154 if (!empty($estr1)) {
155 $errorMessages[] = $estr1;
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;
165 $message = '';
166 if ($errorCode > 0) {
167 $message = '0x' . dechex($errorCode) . ' ';
170 if (count($errorMessages) > 0) {
171 $message .= '(' . implode('; ', $errorMessages) . ')';
172 } else {
173 $message .= '(no error message from LDAP)';
176 return $message;
180 * Get the currently bound user
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
186 * @return bool|null|string
188 public function getBoundUser()
190 return $this->boundUser;
194 * Sets the options used in connecting, binding, etc.
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
214 * @param array|Traversable $options Options used in connecting, binding, etc.
215 * @return Ldap Provides a fluent interface
216 * @throws Exception\LdapException
218 public function setOptions($options)
220 if ($options instanceof Traversable) {
221 $options = iterator_to_array($options);
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,
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).
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;
272 if (count($options) > 0) {
273 $key = key($options);
274 throw new Exception\LdapException(null, "Unknown Zend\\Ldap\\Ldap option: $key");
276 $this->options = $permittedOptions;
278 return $this;
282 * @return array The current options.
284 public function getOptions()
286 return $this->options;
290 * @return string The hostname of the LDAP server being used to
291 * authenticate accounts
293 protected function getHost()
295 return $this->options['host'];
299 * @return int The port of the LDAP server or 0 to indicate that no port
300 * value is set
302 protected function getPort()
304 return $this->options['port'];
308 * @return bool The default SSL / TLS encrypted transport control
310 protected function getUseSsl()
312 return $this->options['useSsl'];
316 * @return string The default acctname for binding
318 protected function getUsername()
320 return $this->options['username'];
324 * @return string The default password for binding
326 protected function getPassword()
328 return $this->options['password'];
332 * @return bool Bind requires DN
334 protected function getBindRequiresDn()
336 return $this->options['bindRequiresDn'];
340 * Gets the base DN under which objects of interest are located
342 * @return string
344 public function getBaseDn()
346 return $this->options['baseDn'];
350 * @return int Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or
351 * ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to.
353 protected function getAccountCanonicalForm()
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.
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;
375 return $accountCanonicalForm;
379 * @return string The account domain name
381 protected function getAccountDomainName()
383 return $this->options['accountDomainName'];
387 * @return string The short account domain name
389 protected function getAccountDomainNameShort()
391 return $this->options['accountDomainNameShort'];
395 * @return string A format string for building an LDAP search filter to match
396 * an account
398 protected function getAccountFilterFormat()
400 return $this->options['accountFilterFormat'];
404 * @return bool Allow empty passwords
406 protected function getAllowEmptyPassword()
408 return $this->options['allowEmptyPassword'];
412 * @return bool The default SSL / TLS encrypted transport control
414 protected function getUseStartTls()
416 return $this->options['useStartTls'];
420 * @return bool Opt. Referrals
422 protected function getOptReferrals()
424 return $this->options['optReferrals'];
428 * @return bool Try splitting the username into username and domain
430 protected function getTryUsernameSplit()
432 return $this->options['tryUsernameSplit'];
436 * @return int The value for network timeout when connect to the LDAP server.
438 protected function getNetworkTimeout()
440 return $this->options['networkTimeout'];
444 * @param string $acctname
445 * @return string The LDAP search filter for matching directory accounts
447 protected function getAccountFilter($acctname)
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);
457 if (!$this->getBindRequiresDn()) {
458 // is there a better way to detect this?
459 return sprintf("(&(objectClass=user)(sAMAccountName=%s))", $aname);
462 return sprintf("(&(objectClass=posixAccount)(uid=%s))", $aname);
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
471 protected function splitName($name, &$dname, &$aname)
473 $dname = null;
474 $aname = $name;
476 if (!$this->getTryUsernameSplit()) {
477 return;
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);
494 * @param string $acctname The name of the account
495 * @return string The DN of the specified account
496 * @throws Exception\LdapException
498 protected function getAccountDn($acctname)
500 if (Dn::checkDn($acctname)) {
501 return $acctname;
503 $acctname = $this->getCanonicalAccountName($acctname, self::ACCTNAME_FORM_USERNAME);
504 $acct = $this->getAccount($acctname, array('dn'));
506 return $acct['dn'];
510 * @param string $dname The domain name to check
511 * @return bool
513 protected function isPossibleAuthority($dname)
515 if ($dname === null) {
516 return true;
518 $accountDomainName = $this->getAccountDomainName();
519 $accountDomainNameShort = $this->getAccountDomainNameShort();
520 if ($accountDomainName === null && $accountDomainNameShort === null) {
521 return true;
523 if (strcasecmp($dname, $accountDomainName) == 0) {
524 return true;
526 if (strcasecmp($dname, $accountDomainNameShort) == 0) {
527 return true;
530 return false;
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
539 public function getCanonicalAccountName($acctname, $form = 0)
541 $dname = '';
542 $uname = '';
544 $this->splitName($acctname, $dname, $uname);
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);
552 if (!$uname) {
553 throw new Exception\LdapException(null, "Invalid account name syntax: $acctname");
556 if (function_exists('mb_strtolower')) {
557 $uname = mb_strtolower($uname, 'UTF-8');
558 } else {
559 $uname = strtolower($uname);
562 if ($form === 0) {
563 $form = $this->getAccountCanonicalForm();
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');
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');
582 return "$uname@$accountDomainName";
583 default:
584 throw new Exception\LdapException(null, "Unknown canonical name form: $form");
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
594 protected function getAccount($acctname, array $attrs = null)
596 $baseDn = $this->getBaseDn();
597 if (!$baseDn) {
598 throw new Exception\LdapException(null, 'Base DN not set');
601 $accountFilter = $this->getAccountFilter($acctname);
602 if (!$accountFilter) {
603 throw new Exception\LdapException(null, 'Invalid account filter');
606 if (!is_resource($this->getResource())) {
607 $this->bind();
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();
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";
626 $accounts->close();
628 throw new Exception\LdapException($this, $str, $code);
632 * @return Ldap Provides a fluent interface
634 public function disconnect()
636 if (is_resource($this->resource)) {
637 ErrorHandler::start(E_WARNING);
638 ldap_unbind($this->resource);
639 ErrorHandler::stop();
641 $this->resource = null;
642 $this->boundUser = false;
644 return $this;
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.
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
662 public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null, $networkTimeout = null)
664 if ($host === null) {
665 $host = $this->getHost();
667 if ($port === null) {
668 $port = $this->getPort();
669 } else {
670 $port = (int) $port;
672 if ($useSsl === null) {
673 $useSsl = $this->getUseSsl();
674 } else {
675 $useSsl = (bool) $useSsl;
677 if ($useStartTls === null) {
678 $useStartTls = $this->getUseStartTls();
679 } else {
680 $useStartTls = (bool) $useStartTls;
682 if ($networkTimeout === null) {
683 $networkTimeout = $this->getNetworkTimeout();
684 } else {
685 $networkTimeout = (int) $networkTimeout;
688 if (!$host) {
689 throw new Exception\LdapException(null, 'A host parameter is required');
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().
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;
709 if ($port) {
710 $this->connectString .= ':' . $port;
714 $this->disconnect();
717 /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just
718 * use the old form.
720 ErrorHandler::start();
721 $resource = ($useUri) ? ldap_connect($this->connectString) : ldap_connect($host, $port);
722 ErrorHandler::stop();
724 if (is_resource($resource) === true) {
725 $this->resource = $resource;
726 $this->boundUser = false;
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)
733 if ($networkTimeout) {
734 ldap_set_option($resource, LDAP_OPT_NETWORK_TIMEOUT, $networkTimeout);
736 if ($useSsl || !$useStartTls || ldap_start_tls($resource)) {
737 ErrorHandler::stop();
738 return $this;
741 ErrorHandler::stop();
743 $zle = new Exception\LdapException($this, "$host:$port");
744 $this->disconnect();
745 throw $zle;
748 throw new Exception\LdapException(null, "Failed to connect to LDAP server: $host:$port");
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
757 public function bind($username = null, $password = null)
759 $moreCreds = true;
761 if ($username === null) {
762 $username = $this->getUsername();
763 $password = $this->getPassword();
764 $moreCreds = false;
767 if (empty($username)) {
768 /* Perform anonymous bind
770 $username = null;
771 $password = null;
772 } else {
773 /* Check to make sure the username is in DN form.
775 if (!Dn::checkDn($username)) {
776 if ($this->getBindRequiresDn()) {
777 /* moreCreds stops an infinite loop if getUsername does not
778 * return a DN and the bind requires it
780 if ($moreCreds) {
781 try {
782 $username = $this->getAccountDn($username);
783 } catch (Exception\LdapException $zle) {
784 switch ($zle->getCode()) {
785 case Exception\LdapException::LDAP_NO_SUCH_OBJECT:
786 case Exception\LdapException::LDAP_X_DOMAIN_MISMATCH:
787 case Exception\LdapException::LDAP_X_EXTENSION_NOT_LOADED:
788 throw $zle;
790 throw new Exception\LdapException(null,
791 'Failed to retrieve DN for account: ' . $username .
792 ' [' . $zle->getMessage() . ']',
793 Exception\LdapException::LDAP_OPERATIONS_ERROR);
795 } else {
796 throw new Exception\LdapException(null, 'Binding requires username in DN form');
798 } else {
799 $username = $this->getCanonicalAccountName(
800 $username,
801 $this->getAccountCanonicalForm()
807 if (!is_resource($this->resource)) {
808 $this->connect();
811 if ($username !== null && $password === '' && $this->getAllowEmptyPassword() !== true) {
812 $zle = new Exception\LdapException(null,
813 'Empty password not allowed - see allowEmptyPassword option.');
814 } else {
815 ErrorHandler::start(E_WARNING);
816 $bind = ldap_bind($this->resource, $username, $password);
817 ErrorHandler::stop();
818 if ($bind) {
819 $this->boundUser = $username;
820 return $this;
823 $message = ($username === null) ? $this->connectString : $username;
824 switch ($this->getLastErrorCode()) {
825 case Exception\LdapException::LDAP_SERVER_DOWN:
826 /* If the error is related to establishing a connection rather than binding,
827 * the connect string is more informative than the username.
829 $message = $this->connectString;
832 $zle = new Exception\LdapException($this, $message);
834 $this->disconnect();
836 throw $zle;
840 * A global LDAP search routine for finding information.
842 * Options can be either passed as single parameters according to the
843 * method signature or as an array with one or more of the following keys
844 * - filter
845 * - baseDn
846 * - scope
847 * - attributes
848 * - sort
849 * - collectionClass
850 * - sizelimit
851 * - timelimit
853 * @param string|Filter\AbstractFilter|array $filter
854 * @param string|Dn|null $basedn
855 * @param int $scope
856 * @param array $attributes
857 * @param string|null $sort
858 * @param string|null $collectionClass
859 * @param int $sizelimit
860 * @param int $timelimit
861 * @return Collection
862 * @throws Exception\LdapException
864 public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, array $attributes = array(),
865 $sort = null, $collectionClass = null, $sizelimit = 0, $timelimit = 0
868 if (is_array($filter)) {
869 $options = array_change_key_case($filter, CASE_LOWER);
870 foreach ($options as $key => $value) {
871 switch ($key) {
872 case 'filter':
873 case 'basedn':
874 case 'scope':
875 case 'sort':
876 $$key = $value;
877 break;
878 case 'attributes':
879 if (is_array($value)) {
880 $attributes = $value;
882 break;
883 case 'collectionclass':
884 $collectionClass = $value;
885 break;
886 case 'sizelimit':
887 case 'timelimit':
888 $$key = (int) $value;
889 break;
894 if ($basedn === null) {
895 $basedn = $this->getBaseDn();
896 } elseif ($basedn instanceof Dn) {
897 $basedn = $basedn->toString();
900 if ($filter instanceof Filter\AbstractFilter) {
901 $filter = $filter->toString();
904 $resource = $this->getResource();
905 ErrorHandler::start(E_WARNING);
906 switch ($scope) {
907 case self::SEARCH_SCOPE_ONE:
908 $search = ldap_list($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
909 break;
910 case self::SEARCH_SCOPE_BASE:
911 $search = ldap_read($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
912 break;
913 case self::SEARCH_SCOPE_SUB:
914 default:
915 $search = ldap_search($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
916 break;
918 ErrorHandler::stop();
920 if ($search === false) {
921 throw new Exception\LdapException($this, 'searching: ' . $filter);
923 if ($sort !== null && is_string($sort)) {
924 ErrorHandler::start(E_WARNING);
925 $isSorted = ldap_sort($resource, $search, $sort);
926 ErrorHandler::stop();
927 if ($isSorted === false) {
928 throw new Exception\LdapException($this, 'sorting: ' . $sort);
932 $iterator = new Collection\DefaultIterator($this, $search);
934 return $this->createCollection($iterator, $collectionClass);
938 * Extension point for collection creation
940 * @param Collection\DefaultIterator $iterator
941 * @param string|null $collectionClass
942 * @return Collection
943 * @throws Exception\LdapException
945 protected function createCollection(Collection\DefaultIterator $iterator, $collectionClass)
947 if ($collectionClass === null) {
948 return new Collection($iterator);
949 } else {
950 $collectionClass = (string) $collectionClass;
951 if (!class_exists($collectionClass)) {
952 throw new Exception\LdapException(null,
953 "Class '$collectionClass' can not be found");
955 if (!is_subclass_of($collectionClass, 'Zend\Ldap\Collection')) {
956 throw new Exception\LdapException(null,
957 "Class '$collectionClass' must subclass 'Zend\\Ldap\\Collection'");
960 return new $collectionClass($iterator);
965 * Count items found by given filter.
967 * @param string|Filter\AbstractFilter $filter
968 * @param string|Dn|null $basedn
969 * @param int $scope
970 * @return int
971 * @throws Exception\LdapException
973 public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB)
975 try {
976 $result = $this->search($filter, $basedn, $scope, array('dn'), null);
977 } catch (Exception\LdapException $e) {
978 if ($e->getCode() === Exception\LdapException::LDAP_NO_SUCH_OBJECT) {
979 return 0;
981 throw $e;
984 return $result->count();
988 * Count children for a given DN.
990 * @param string|Dn $dn
991 * @return int
992 * @throws Exception\LdapException
994 public function countChildren($dn)
996 return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE);
1000 * Check if a given DN exists.
1002 * @param string|Dn $dn
1003 * @return bool
1004 * @throws Exception\LdapException
1006 public function exists($dn)
1008 return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE) == 1);
1012 * Search LDAP registry for entries matching filter and optional attributes
1014 * Options can be either passed as single parameters according to the
1015 * method signature or as an array with one or more of the following keys
1016 * - filter
1017 * - baseDn
1018 * - scope
1019 * - attributes
1020 * - sort
1021 * - reverseSort
1022 * - sizelimit
1023 * - timelimit
1025 * @param string|Filter\AbstractFilter|array $filter
1026 * @param string|Dn|null $basedn
1027 * @param int $scope
1028 * @param array $attributes
1029 * @param string|null $sort
1030 * @param bool $reverseSort
1031 * @param int $sizelimit
1032 * @param int $timelimit
1033 * @return array
1034 * @throws Exception\LdapException
1036 public function searchEntries($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB,
1037 array $attributes = array(), $sort = null, $reverseSort = false, $sizelimit = 0,
1038 $timelimit = 0)
1040 if (is_array($filter)) {
1041 $filter = array_change_key_case($filter, CASE_LOWER);
1042 if (isset($filter['collectionclass'])) {
1043 unset($filter['collectionclass']);
1045 if (isset($filter['reversesort'])) {
1046 $reverseSort = $filter['reversesort'];
1047 unset($filter['reversesort']);
1050 $result = $this->search($filter, $basedn, $scope, $attributes, $sort, null, $sizelimit, $timelimit);
1051 $items = $result->toArray();
1052 if ((bool) $reverseSort === true) {
1053 $items = array_reverse($items, false);
1056 return $items;
1060 * Get LDAP entry by DN
1062 * @param string|Dn $dn
1063 * @param array $attributes
1064 * @param bool $throwOnNotFound
1065 * @return array
1066 * @throws null|Exception\LdapException
1068 public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false)
1070 try {
1071 $result = $this->search(
1072 "(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE,
1073 $attributes, null
1076 return $result->getFirst();
1077 } catch (Exception\LdapException $e) {
1078 if ($throwOnNotFound !== false) {
1079 throw $e;
1083 return null;
1087 * Prepares an ldap data entry array for insert/update operation
1089 * @param array $entry
1090 * @throws Exception\InvalidArgumentException
1091 * @return void
1093 public static function prepareLdapEntryArray(array &$entry)
1095 if (array_key_exists('dn', $entry)) {
1096 unset($entry['dn']);
1098 foreach ($entry as $key => $value) {
1099 if (is_array($value)) {
1100 foreach ($value as $i => $v) {
1101 if ($v === null) {
1102 unset($value[$i]);
1103 } elseif (!is_scalar($v)) {
1104 throw new Exception\InvalidArgumentException('Only scalar values allowed in LDAP data');
1105 } else {
1106 $v = (string) $v;
1107 if (strlen($v) == 0) {
1108 unset($value[$i]);
1109 } else {
1110 $value[$i] = $v;
1114 $entry[$key] = array_values($value);
1115 } else {
1116 if ($value === null) {
1117 $entry[$key] = array();
1118 } elseif (!is_scalar($value)) {
1119 throw new Exception\InvalidArgumentException('Only scalar values allowed in LDAP data');
1120 } else {
1121 $value = (string) $value;
1122 if (strlen($value) == 0) {
1123 $entry[$key] = array();
1124 } else {
1125 $entry[$key] = array($value);
1130 $entry = array_change_key_case($entry, CASE_LOWER);
1134 * Add new information to the LDAP repository
1136 * @param string|Dn $dn
1137 * @param array $entry
1138 * @return Ldap Provides a fluid interface
1139 * @throws Exception\LdapException
1141 public function add($dn, array $entry)
1143 if (!($dn instanceof Dn)) {
1144 $dn = Dn::factory($dn, null);
1146 static::prepareLdapEntryArray($entry);
1147 foreach ($entry as $key => $value) {
1148 if (is_array($value) && count($value) === 0) {
1149 unset($entry[$key]);
1153 $rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER);
1154 foreach ($rdnParts as $key => $value) {
1155 $value = Dn::unescapeValue($value);
1156 if (!array_key_exists($key, $entry)) {
1157 $entry[$key] = array($value);
1158 } elseif (!in_array($value, $entry[$key])) {
1159 $entry[$key] = array_merge(array($value), $entry[$key]);
1162 $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
1163 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
1164 foreach ($adAttributes as $attr) {
1165 if (array_key_exists($attr, $entry)) {
1166 unset($entry[$attr]);
1170 $resource = $this->getResource();
1171 ErrorHandler::start(E_WARNING);
1172 $isAdded = ldap_add($resource, $dn->toString(), $entry);
1173 ErrorHandler::stop();
1174 if ($isAdded === false) {
1175 throw new Exception\LdapException($this, 'adding: ' . $dn->toString());
1178 return $this;
1182 * Update LDAP registry
1184 * @param string|Dn $dn
1185 * @param array $entry
1186 * @return Ldap Provides a fluid interface
1187 * @throws Exception\LdapException
1189 public function update($dn, array $entry)
1191 if (!($dn instanceof Dn)) {
1192 $dn = Dn::factory($dn, null);
1194 static::prepareLdapEntryArray($entry);
1196 $rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER);
1197 foreach ($rdnParts as $key => $value) {
1198 $value = Dn::unescapeValue($value);
1199 if (array_key_exists($key, $entry) && !in_array($value, $entry[$key])) {
1200 $entry[$key] = array_merge(array($value), $entry[$key]);
1203 $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
1204 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
1205 foreach ($adAttributes as $attr) {
1206 if (array_key_exists($attr, $entry)) {
1207 unset($entry[$attr]);
1211 if (count($entry) > 0) {
1212 $resource = $this->getResource();
1213 ErrorHandler::start(E_WARNING);
1214 $isModified = ldap_modify($resource, $dn->toString(), $entry);
1215 ErrorHandler::stop();
1216 if ($isModified === false) {
1217 throw new Exception\LdapException($this, 'updating: ' . $dn->toString());
1221 return $this;
1225 * Save entry to LDAP registry.
1227 * Internally decides if entry will be updated to added by calling
1228 * {@link exists()}.
1230 * @param string|Dn $dn
1231 * @param array $entry
1232 * @return Ldap Provides a fluid interface
1233 * @throws Exception\LdapException
1235 public function save($dn, array $entry)
1237 if ($dn instanceof Dn) {
1238 $dn = $dn->toString();
1240 if ($this->exists($dn)) {
1241 $this->update($dn, $entry);
1242 } else {
1243 $this->add($dn, $entry);
1246 return $this;
1250 * Delete an LDAP entry
1252 * @param string|Dn $dn
1253 * @param bool $recursively
1254 * @return Ldap Provides a fluid interface
1255 * @throws Exception\LdapException
1257 public function delete($dn, $recursively = false)
1259 if ($dn instanceof Dn) {
1260 $dn = $dn->toString();
1262 if ($recursively === true) {
1263 if ($this->countChildren($dn) > 0) {
1264 $children = $this->getChildrenDns($dn);
1265 foreach ($children as $c) {
1266 $this->delete($c, true);
1271 $resource = $this->getResource();
1272 ErrorHandler::start(E_WARNING);
1273 $isDeleted = ldap_delete($resource, $dn);
1274 ErrorHandler::stop();
1275 if ($isDeleted === false) {
1276 throw new Exception\LdapException($this, 'deleting: ' . $dn);
1279 return $this;
1283 * Retrieve the immediate children DNs of the given $parentDn
1285 * This method is used in recursive methods like {@see delete()}
1286 * or {@see copy()}
1288 * @param string|Dn $parentDn
1289 * @throws Exception\LdapException
1290 * @return array of DNs
1292 protected function getChildrenDns($parentDn)
1294 if ($parentDn instanceof Dn) {
1295 $parentDn = $parentDn->toString();
1297 $children = array();
1299 $resource = $this->getResource();
1300 ErrorHandler::start(E_WARNING);
1301 $search = ldap_list($resource, $parentDn, '(objectClass=*)', array('dn'));
1302 for (
1303 $entry = ldap_first_entry($resource, $search);
1304 $entry !== false;
1305 $entry = ldap_next_entry($resource, $entry)
1307 $childDn = ldap_get_dn($resource, $entry);
1308 if ($childDn === false) {
1309 ErrorHandler::stop();
1310 throw new Exception\LdapException($this, 'getting dn');
1312 $children[] = $childDn;
1314 ldap_free_result($search);
1315 ErrorHandler::stop();
1317 return $children;
1321 * Moves a LDAP entry from one DN to another subtree.
1323 * @param string|Dn $from
1324 * @param string|Dn $to
1325 * @param bool $recursively
1326 * @param bool $alwaysEmulate
1327 * @return Ldap Provides a fluid interface
1328 * @throws Exception\LdapException
1330 public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false)
1332 if ($from instanceof Dn) {
1333 $orgDnParts = $from->toArray();
1334 } else {
1335 $orgDnParts = Dn::explodeDn($from);
1338 if ($to instanceof Dn) {
1339 $newParentDnParts = $to->toArray();
1340 } else {
1341 $newParentDnParts = Dn::explodeDn($to);
1344 $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
1345 $newDn = Dn::fromArray($newDnParts);
1347 return $this->rename($from, $newDn, $recursively, $alwaysEmulate);
1351 * Moves a LDAP entry from one DN to another DN.
1353 * This is an alias for {@link rename()}
1355 * @param string|Dn $from
1356 * @param string|Dn $to
1357 * @param bool $recursively
1358 * @param bool $alwaysEmulate
1359 * @return Ldap Provides a fluid interface
1360 * @throws Exception\LdapException
1362 public function move($from, $to, $recursively = false, $alwaysEmulate = false)
1364 return $this->rename($from, $to, $recursively, $alwaysEmulate);
1368 * Renames a LDAP entry from one DN to another DN.
1370 * This method implicitly moves the entry to another location within the tree.
1372 * @param string|Dn $from
1373 * @param string|Dn $to
1374 * @param bool $recursively
1375 * @param bool $alwaysEmulate
1376 * @return Ldap Provides a fluid interface
1377 * @throws Exception\LdapException
1379 public function rename($from, $to, $recursively = false, $alwaysEmulate = false)
1381 $emulate = (bool) $alwaysEmulate;
1382 if (!function_exists('ldap_rename')) {
1383 $emulate = true;
1384 } elseif ($recursively) {
1385 $emulate = true;
1388 if ($emulate === false) {
1389 if ($from instanceof Dn) {
1390 $from = $from->toString();
1393 if ($to instanceof Dn) {
1394 $newDnParts = $to->toArray();
1395 } else {
1396 $newDnParts = Dn::explodeDn($to);
1399 $newRdn = Dn::implodeRdn(array_shift($newDnParts));
1400 $newParent = Dn::implodeDn($newDnParts);
1402 $resource = $this->getResource();
1403 ErrorHandler::start(E_WARNING);
1404 $isOK = ldap_rename($resource, $from, $newRdn, $newParent, true);
1405 ErrorHandler::stop();
1406 if ($isOK === false) {
1407 throw new Exception\LdapException($this, 'renaming ' . $from . ' to ' . $to);
1408 } elseif (!$this->exists($to)) {
1409 $emulate = true;
1412 if ($emulate) {
1413 $this->copy($from, $to, $recursively);
1414 $this->delete($from, $recursively);
1417 return $this;
1421 * Copies a LDAP entry from one DN to another subtree.
1423 * @param string|Dn $from
1424 * @param string|Dn $to
1425 * @param bool $recursively
1426 * @return Ldap Provides a fluid interface
1427 * @throws Exception\LdapException
1429 public function copyToSubtree($from, $to, $recursively = false)
1431 if ($from instanceof Dn) {
1432 $orgDnParts = $from->toArray();
1433 } else {
1434 $orgDnParts = Dn::explodeDn($from);
1437 if ($to instanceof Dn) {
1438 $newParentDnParts = $to->toArray();
1439 } else {
1440 $newParentDnParts = Dn::explodeDn($to);
1443 $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
1444 $newDn = Dn::fromArray($newDnParts);
1446 return $this->copy($from, $newDn, $recursively);
1450 * Copies a LDAP entry from one DN to another DN.
1452 * @param string|Dn $from
1453 * @param string|Dn $to
1454 * @param bool $recursively
1455 * @return Ldap Provides a fluid interface
1456 * @throws Exception\LdapException
1458 public function copy($from, $to, $recursively = false)
1460 $entry = $this->getEntry($from, array(), true);
1462 if ($to instanceof Dn) {
1463 $toDnParts = $to->toArray();
1464 } else {
1465 $toDnParts = Dn::explodeDn($to);
1467 $this->add($to, $entry);
1469 if ($recursively === true && $this->countChildren($from) > 0) {
1470 $children = $this->getChildrenDns($from);
1471 foreach ($children as $c) {
1472 $cDnParts = Dn::explodeDn($c);
1473 $newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts);
1474 $newChild = Dn::implodeDn($newChildParts);
1475 $this->copy($c, $newChild, true);
1479 return $this;
1483 * Returns the specified DN as a Zend\Ldap\Node
1485 * @param string|Dn $dn
1486 * @return Node|null
1487 * @throws Exception\LdapException
1489 public function getNode($dn)
1491 return Node::fromLdap($dn, $this);
1495 * Returns the base node as a Zend\Ldap\Node
1497 * @return Node
1498 * @throws Exception\LdapException
1500 public function getBaseNode()
1502 return $this->getNode($this->getBaseDn(), $this);
1506 * Returns the RootDse
1508 * @return Node\RootDse
1509 * @throws Exception\LdapException
1511 public function getRootDse()
1513 if ($this->rootDse === null) {
1514 $this->rootDse = Node\RootDse::create($this);
1517 return $this->rootDse;
1521 * Returns the schema
1523 * @return Node\Schema
1524 * @throws Exception\LdapException
1526 public function getSchema()
1528 if ($this->schema === null) {
1529 $this->schema = Node\Schema::create($this);
1532 return $this->schema;