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
13 use Zend\Stdlib\ErrorHandler
;
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;
27 * String used with ldap_connect for error handling purposes.
31 private $connectString;
34 * The options used in connecting, binding, etc.
38 protected $options = null;
41 * The raw LDAP extension resource.
45 protected $resource = null;
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;
61 protected $rootDse = null;
68 protected $schema = null;
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);
90 public function __destruct()
96 * @return resource The raw LDAP extension resource.
98 public function getResource()
100 if (!is_resource($this->resource) ||
$this->boundUser
=== false) {
104 return $this->resource;
108 * Return the LDAP error number of the last LDAP command
112 public function getLastErrorCode()
114 ErrorHandler
::start(E_WARNING
);
115 $ret = ldap_get_option($this->resource, LDAP_OPT_ERROR_NUMBER
, $err);
116 ErrorHandler
::stop();
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);
131 * Return the LDAP error message of the last LDAP command
133 * @param int $errorCode
134 * @param array $errorMessages
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;
166 if ($errorCode > 0) {
167 $message = '0x' . dechex($errorCode) . ' ';
170 if (count($errorMessages) > 0) {
171 $message .= '(' . implode('; ', $errorMessages) . ')';
173 $message .= '(no error message from LDAP)';
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.
204 * accountCanonicalForm
206 * accountDomainNameShort
207 * accountFilterFormat
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(
230 'bindRequiresDn' => false,
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).
252 case 'accountCanonicalForm':
253 case 'networkTimeout':
254 $permittedOptions[$key] = (int) $val;
257 case 'bindRequiresDn':
258 case 'allowEmptyPassword':
261 case 'tryUsernameSplit':
262 $permittedOptions[$key] = ($val === true
264 ||
strcasecmp($val, 'true') == 0);
267 $permittedOptions[$key] = trim($val);
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;
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
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
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
;
367 if ($accountDomainName) {
368 $accountCanonicalForm = self
::ACCTNAME_FORM_PRINCIPAL
;
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
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)
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)
471 protected function splitName($name, &$dname, &$aname)
476 if (!$this->getTryUsernameSplit()) {
480 $pos = strpos($name, '@');
482 $dname = substr($name, $pos +
1);
483 $aname = substr($name, 0, $pos);
485 $pos = strpos($name, '\\');
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)) {
503 $acctname = $this->getCanonicalAccountName($acctname, self
::ACCTNAME_FORM_USERNAME
);
504 $acct = $this->getAccount($acctname, array('dn'));
510 * @param string $dname The domain name to check
513 protected function isPossibleAuthority($dname)
515 if ($dname === null) {
518 $accountDomainName = $this->getAccountDomainName();
519 $accountDomainNameShort = $this->getAccountDomainNameShort();
520 if ($accountDomainName === null && $accountDomainNameShort === null) {
523 if (strcasecmp($dname, $accountDomainName) == 0) {
526 if (strcasecmp($dname, $accountDomainNameShort) == 0) {
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)
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
);
553 throw new Exception\
LdapException(null, "Invalid account name syntax: $acctname");
556 if (function_exists('mb_strtolower')) {
557 $uname = mb_strtolower($uname, 'UTF-8');
559 $uname = strtolower($uname);
563 $form = $this->getAccountCanonicalForm();
567 case self
::ACCTNAME_FORM_DN
:
568 return $this->getAccountDn($acctname);
569 case self
::ACCTNAME_FORM_USERNAME
:
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";
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();
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())) {
610 $accounts = $this->search($accountFilter, $baseDn, self
::SEARCH_SCOPE_SUB
, $attrs);
611 $count = $accounts->count();
613 $acct = $accounts->getFirst();
619 $code = Exception\LdapException
::LDAP_NO_SUCH_OBJECT
;
620 $str = "No object found for: $accountFilter";
622 $code = Exception\LdapException
::LDAP_OPERATIONS_ERROR
;
623 $str = "Unexpected result count ($count) for: $accountFilter";
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;
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
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();
672 if ($useSsl === null) {
673 $useSsl = $this->getUseSsl();
675 $useSsl = (bool) $useSsl;
677 if ($useStartTls === null) {
678 $useStartTls = $this->getUseStartTls();
680 $useStartTls = (bool) $useStartTls;
682 if ($networkTimeout === null) {
683 $networkTimeout = $this->getNetworkTimeout();
685 $networkTimeout = (int) $networkTimeout;
689 throw new Exception\
LdapException(null, 'A host parameter is required');
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().
698 if (preg_match_all('~ldap(?:i|s)?://~', $host, $hosts, PREG_SET_ORDER
) > 0) {
699 $this->connectString
= $host;
704 $this->connectString
= 'ldaps://' . $host;
707 $this->connectString
= 'ldap://' . $host;
710 $this->connectString
.= ':' . $port;
717 /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just
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();
741 ErrorHandler
::stop();
743 $zle = new Exception\
LdapException($this, "$host:$port");
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)
761 if ($username === null) {
762 $username = $this->getUsername();
763 $password = $this->getPassword();
767 if (empty($username)) {
768 /* Perform anonymous bind
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
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
:
790 throw new Exception\
LdapException(null,
791 'Failed to retrieve DN for account: ' . $username .
792 ' [' . $zle->getMessage() . ']',
793 Exception\LdapException
::LDAP_OPERATIONS_ERROR
);
796 throw new Exception\
LdapException(null, 'Binding requires username in DN form');
799 $username = $this->getCanonicalAccountName(
801 $this->getAccountCanonicalForm()
807 if (!is_resource($this->resource)) {
811 if ($username !== null && $password === '' && $this->getAllowEmptyPassword() !== true) {
812 $zle = new Exception\
LdapException(null,
813 'Empty password not allowed - see allowEmptyPassword option.');
815 ErrorHandler
::start(E_WARNING
);
816 $bind = ldap_bind($this->resource, $username, $password);
817 ErrorHandler
::stop();
819 $this->boundUser
= $username;
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);
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
853 * @param string|Filter\AbstractFilter|array $filter
854 * @param string|Dn|null $basedn
856 * @param array $attributes
857 * @param string|null $sort
858 * @param string|null $collectionClass
859 * @param int $sizelimit
860 * @param int $timelimit
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) {
879 if (is_array($value)) {
880 $attributes = $value;
883 case 'collectionclass':
884 $collectionClass = $value;
888 $
$key = (int) $value;
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
);
907 case self
::SEARCH_SCOPE_ONE
:
908 $search = ldap_list($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
910 case self
::SEARCH_SCOPE_BASE
:
911 $search = ldap_read($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
913 case self
::SEARCH_SCOPE_SUB
:
915 $search = ldap_search($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
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
943 * @throws Exception\LdapException
945 protected function createCollection(Collection\DefaultIterator
$iterator, $collectionClass)
947 if ($collectionClass === null) {
948 return new Collection($iterator);
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
971 * @throws Exception\LdapException
973 public function count($filter, $basedn = null, $scope = self
::SEARCH_SCOPE_SUB
)
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
) {
984 return $result->count();
988 * Count children for a given DN.
990 * @param string|Dn $dn
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
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
1025 * @param string|Filter\AbstractFilter|array $filter
1026 * @param string|Dn|null $basedn
1028 * @param array $attributes
1029 * @param string|null $sort
1030 * @param bool $reverseSort
1031 * @param int $sizelimit
1032 * @param int $timelimit
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,
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);
1060 * Get LDAP entry by DN
1062 * @param string|Dn $dn
1063 * @param array $attributes
1064 * @param bool $throwOnNotFound
1066 * @throws null|Exception\LdapException
1068 public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false)
1071 $result = $this->search(
1072 "(objectClass=*)", $dn, self
::SEARCH_SCOPE_BASE
,
1076 return $result->getFirst();
1077 } catch (Exception\LdapException
$e) {
1078 if ($throwOnNotFound !== false) {
1087 * Prepares an ldap data entry array for insert/update operation
1089 * @param array $entry
1090 * @throws Exception\InvalidArgumentException
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) {
1103 } elseif (!is_scalar($v)) {
1104 throw new Exception\
InvalidArgumentException('Only scalar values allowed in LDAP data');
1107 if (strlen($v) == 0) {
1114 $entry[$key] = array_values($value);
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');
1121 $value = (string) $value;
1122 if (strlen($value) == 0) {
1123 $entry[$key] = array();
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());
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());
1225 * Save entry to LDAP registry.
1227 * Internally decides if entry will be updated to added by calling
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);
1243 $this->add($dn, $entry);
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);
1283 * Retrieve the immediate children DNs of the given $parentDn
1285 * This method is used in recursive methods like {@see delete()}
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'));
1303 $entry = ldap_first_entry($resource, $search);
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();
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();
1335 $orgDnParts = Dn
::explodeDn($from);
1338 if ($to instanceof Dn
) {
1339 $newParentDnParts = $to->toArray();
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')) {
1384 } elseif ($recursively) {
1388 if ($emulate === false) {
1389 if ($from instanceof Dn
) {
1390 $from = $from->toString();
1393 if ($to instanceof Dn
) {
1394 $newDnParts = $to->toArray();
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)) {
1413 $this->copy($from, $to, $recursively);
1414 $this->delete($from, $recursively);
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();
1434 $orgDnParts = Dn
::explodeDn($from);
1437 if ($to instanceof Dn
) {
1438 $newParentDnParts = $to->toArray();
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();
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);
1483 * Returns the specified DN as a Zend\Ldap\Node
1485 * @param string|Dn $dn
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
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
;