diff --git a/api/src/Accounts.php b/api/src/Accounts.php index 4151043178..3c701ced5e 100644 --- a/api/src/Accounts.php +++ b/api/src/Accounts.php @@ -1048,7 +1048,8 @@ class Accounts } return False; } -/** + + /** * Get a list of how many entries of each app the account has * * @param int $account_id @@ -1102,6 +1103,7 @@ class Accounts return $counts; } + protected function get_owner_columns() { $owner_columns = array(); diff --git a/api/src/Accounts/Ads.php b/api/src/Accounts/Ads.php index f3c5e14c79..48397c6920 100644 --- a/api/src/Accounts/Ads.php +++ b/api/src/Accounts/Ads.php @@ -14,6 +14,7 @@ namespace EGroupware\Api\Accounts; use EGroupware\Api; +use EGroupware\Api\Ldap\ServerInfo; require_once EGW_INCLUDE_ROOT.'/vendor/adldap2/adldap2/src/adLDAP.php'; use adLDAPException; @@ -90,7 +91,7 @@ class Ads protected static $user_attributes = array( 'objectsid', 'samaccounttype', 'samaccountname', 'primarygroupid', 'givenname', 'sn', 'mail', 'displayname', 'telephonenumber', - 'objectguid', 'useraccountcontrol', 'accountexpires', 'pwdlastset', 'whencreated', 'whenchanged', + 'objectguid', 'useraccountcontrol', 'accountexpires', 'pwdlastset', 'whencreated', 'whenchanged', 'lastlogon', ); /** @@ -108,6 +109,44 @@ class Ads */ const MIN_ACCOUNT_ID = 1000; + + /** + * Timestamps ldap => egw used in several places + * + * @var string[] + */ + public $timestamps2egw = [ + 'whencreated' => 'account_created', + 'whenchanged' => 'account_modified', + 'accountexpires' => 'account_expires', + 'lastlogon' => 'account_lastlogin', + ]; + + /** + * Other attributes sorted by their default matching rule + */ + public $other2egw = [ + 'primarygroupid' => 'account_primary_group', + ]; + + /** + * String attributes which can be sorted by caseIgnoreMatch ldap => egw + * + * @var string[] + */ + public $attributes2egw = [ + 'samaccountname' => 'account_lid', + 'sn' => 'account_lastname', + 'givenname' => 'account_firstname', + 'displayname' => 'account_fullname', + 'mail' => 'account_email', + ]; + + /** + * @var ServerInfo + */ + public $serverinfo; + /** * Enable extra debug messages via error_log (error always get logged) */ @@ -124,6 +163,8 @@ class Ads $this->frontend = $frontend; $this->adldap = self::get_adldap($this->frontend->config); + + $this->serverinfo = ServerInfo::get($this->ldap_connection(), $this->frontend->config['ads_host']); } /** @@ -509,6 +550,8 @@ class Ads $this->adldap->utilities()->convertWindowsTimeToUnixTime($data['accountexpires'][0]), 'account_lastpwd_change' => !isset($data['pwdlastset']) ? null : (!$data['pwdlastset'][0] ? 0 : $this->adldap->utilities()->convertWindowsTimeToUnixTime($data['pwdlastset'][0])), + 'account_lastlogin' => empty($data['lastlogon'][0]) ? null : + $this->adldap->utilities()->convertWindowsTimeToUnixTime($data['lastlogon'][0]), 'account_created' => !isset($data['whencreated'][0]) ? null : self::_when2ts($data['whencreated'][0]), 'account_modified' => !isset($data['whenchanged'][0]) ? null : @@ -784,7 +827,7 @@ class Ads self::convertUnixTimeToWindowsTime($data[$egw]); break; case 'account_status': - if ($new_entry && empty($data['account_passwd'])) continue; // cant active new account without passwd! + if ($new_entry && empty($data['account_passwd'])) continue 2; // cant active new account without passwd! $attributes[$adldap] = $data[$egw] == 'A'; break; case 'account_lastpwd_change': @@ -847,13 +890,14 @@ class Ads * 'lid','firstname','lastname','email' - query only the given field for containing $param[query] * @param $param['offset'] int - number of matches to return if start given, default use the value in the prefs * @param $param['objectclass'] boolean return objectclass(es) under key 'objectclass' in each account + * @param $param['active'] boolean true: only return active / not expired accounts * @return array with account_id => data pairs, data is an array with account_id, account_lid, account_firstname, * account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group */ function search($param) { - //error_log(__METHOD__.'('.array2string($param).')'); - $account_search = &$this->cache['account_search']; + //error_log(__METHOD__.'('.json_encode($param).') '.function_backtrace()); + $account_search = []; // disabled, we have sorted&limited queries now &$this->cache['account_search']; // check if the query is cached $serial = serialize($param); @@ -876,6 +920,7 @@ class Ads } else // we need to run the unlimited query { + $this->total = null; $query = Api\Ldap::quote(strtolower($param['query'])); $accounts = array(); @@ -902,7 +947,7 @@ class Ads static $to_ldap = array( 'firstname' => 'givenname', 'lastname' => 'sn', - 'lid' => 'uid', + 'lid' => 'samaccountname', 'email' => 'mail', ); $filter = '('.$to_ldap[$param['query_type']].'=*'.$query.'*)'; @@ -914,13 +959,9 @@ class Ads $membership_filter = '(|(memberOf='.$this->id2name((int)$param['type'], 'account_dn').')(PrimaryGroupId='.abs($param['type']).'))'; $filter = $filter ? "(&$membership_filter$filter)" : $membership_filter; } - foreach($this->filter($filter, 'u', self::$user_attributes) as $account_id => $data) + foreach($this->filter($filter, 'u', self::$user_attributes, [], $param['active'], $param['order'].' '.$param['sort'], $start, $offset, $this->total) as $account_id => $data) { $account = $this->_ldap2user($data); - if ($param['active'] && !$this->frontend->is_active($account)) - { - continue; - } $account['account_fullname'] = Api\Accounts::format_username($account['account_lid'],$account['account_firstname'],$account['account_lastname'],$account['account_id']); $accounts[$account_id] = $account; } @@ -962,7 +1003,7 @@ class Ads uasort($sortedAccounts,array($this,'_sort_callback')); $account_search[$unl_serial]['data'] = $sortedAccounts; - $account_search[$unl_serial]['total'] = $this->total = count($accounts); + $account_search[$unl_serial]['total'] = $this->total = $this->total ?? count($accounts); } // return only the wanted accounts reset($sortedAccounts); @@ -1021,19 +1062,26 @@ class Ads * Get LDAP filter for user, groups or both * * @param string|null $account_type u = user, g = group, default null = try both + * @param bool $filter_expired =false true: filter out expired users * @return string string with LDAP filter */ - public function type_filter($account_type=null) + public function type_filter($account_type=null, $filter_expired=false) { switch ($account_type) { default: // user or groups case 'u': - $type_filter = '(samaccounttype=' . adLDAP::ADLDAP_NORMAL_ACCOUNT . ')'; + $type_filter = '(&(samaccounttype=' . adLDAP::ADLDAP_NORMAL_ACCOUNT . ')'; + $type_filter .= '(!(isCriticalSystemObject=*))'; // exclude stock users (eg. Administrator) and groups + if ($filter_expired) + { + $type_filter .= '(|(!(accountExpires=*))(accountExpires=0)(accountExpires>='.self::convertUnixTimeToWindowsTime(time()).'))'; + } if (!empty($this->frontend->config['ads_user_filter'])) { - $type_filter = '(&' . $type_filter . $this->frontend->config['ads_user_filter'] . ')'; + $type_filter .= $this->frontend->config['ads_user_filter']; } + $type_filter .= ')'; if ($account_type === 'u') break; $user_filter = $type_filter; // fall through @@ -1052,6 +1100,40 @@ class Ads return $type_filter; } + /** + * Get value(s) for LDAP_CONTROL_SORTREQUEST + * + * @param ?string $order_by sql order string eg. "contact_email ASC" + * @return array of arrays with values for keys 'attr', 'oid' (caseIgnoreMatch='2.5.13.3') and 'reverse' + * @todo sorting by multiple criteria is supported in LDAP RFC 2891, but - at least with Univention - gives wired results + */ + protected function sort_values($order_by) + { + $values = []; + while (!empty($order_by) && preg_match("/^(account_)?([^ ]+)( ASC| DESC)?,?/i", $order_by, $matches)) + { + if (($attr = array_search('account_'.$matches[2], $this->timestamps2egw+$this->other2egw))) + { + $values[] = [ + 'attr' => $attr, + // use default match 'oid' => '', + 'reverse' => strtoupper($matches[3]) === ' DESC', + ]; + } + elseif (($attr = array_search('account_'.$matches[2], $this->attributes2egw))) + { + $values[] = [ + 'attr' => $attr, + 'oid' => '2.5.13.3', // caseIgnoreMatch + 'reverse' => strtoupper($matches[3]) === ' DESC', + ]; + } + $order_by = substr($order_by, strlen($matches[0])); + if ($values) break; // sorting by multiple criteria gives wired results + } + return $values; + } + /** * Query ADS by (optional) filter and (optional) account-type filter * @@ -1061,13 +1143,42 @@ class Ads * @param string $account_type u = user, g = group, default null = try both * @param array $attrs =null default return account_lid, else return raw values from ldap-query * @param array $accounts =array() array to add filtered accounts too, default empty array + * @param bool $filter_expired =false true: filter out expired users + * @param string $order_by sql order string eg. "contact_email ASC" + * @param ?int $start on return null, if result sorted and limited by server + * @param int $num_rows number of rows to return if isset($start) + * @param ?int $total on return total number of rows * @return array account_id => account_lid or values for $attrs pairs */ - protected function filter($attr_filter, $account_type=null, array $attrs=null, array $accounts=array()) + protected function filter($attr_filter, $account_type=null, array $attrs=null, array $accounts=array(), $filter_expired=false, $order_by=null, &$start=null, $num_rows=null, &$total=null) { + // check if we require sorting and server supports it + $control = []; + if (PHP_VERSION >= 7.3 && !empty($order_by) && is_numeric($start) && $this->serverinfo->supportedControl(LDAP_CONTROL_SORTREQUEST, LDAP_CONTROL_VLVREQUEST) && + ($sort_values = $this->sort_values($order_by))) + { + $control = [ + [ + 'oid' => LDAP_CONTROL_SORTREQUEST, + //'iscritical' => TRUE, + 'value' => $sort_values, + ], + [ + 'oid' => LDAP_CONTROL_VLVREQUEST, + //'iscritical' => TRUE, + 'value' => [ + 'before' => 0, // Return 0 entry before target + 'after' => $num_rows-1, // total-1 + 'offset' => $start+1, // first = 1, NOT 0! + 'count' => 0, // We have no idea how many entries there are + ] + ] + ]; + } + if (!$attr_filter) { - $filter = $this->type_filter($account_type); + $filter = $this->type_filter($account_type, $filter_expired); } else { @@ -1086,33 +1197,31 @@ class Ads $filter .= $this->type_filter($account_type).')'; } $sri = ldap_search($ds=$this->ldap_connection(), $context=$this->ads_context(), $filter, - $attrs ? $attrs : self::$default_attributes); + $attrs ? $attrs : self::$default_attributes, null, null, null, null, $control); if (!$sri) { if (self::$debug) error_log(__METHOD__.'('.array2string($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter') returned ".array2string($sri).' trying to reconnect ...'); $sri = ldap_search($ds=$this->ldap_connection(true), $context=$this->ads_context(), $filter, - $attrs ? $attrs : self::$default_attributes); + $attrs ? $attrs : self::$default_attributes, null, null, null, null, $control); } if ($sri && ($allValues = ldap_get_entries($ds, $sri))) { + // check if given controls succeeded + if ($control && ldap_parse_result($ds, $sri, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) && + (isset($serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count']))) + { + $total = $serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count']; + $start = null; // so caller does NOT run it's own limit + } + foreach($allValues as $key => $data) { if ($key === 'count') continue; - if ($account_type && !($account_type == 'u' && $data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT || - $account_type == 'g' && in_array($data['samaccounttype'][0], - [adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP, adLDAP::ADLDAP_SECURITY_LOCAL_GROUP]))) - { - continue; - } $sid = $data['objectsid'] = $this->adldap->utilities()->getTextSID($data['objectsid'][0]); $rid = self::sid2account_id($sid); - if ($data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT && $rid < self::MIN_ACCOUNT_ID) - { - continue; // ignore system accounts incl. "Administrator" - } $accounts[($data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT ? '' : '-').$rid] = $attrs ? $data : Api\Translation::convert($data['samaccountname'][0], 'utf-8'); } diff --git a/api/src/Contacts/Ads.php b/api/src/Contacts/Ads.php index e836ed78ef..10619ec5fc 100644 --- a/api/src/Contacts/Ads.php +++ b/api/src/Contacts/Ads.php @@ -64,7 +64,7 @@ class Ads extends Ldap /** * Accounts ADS object * - * @var Api\Accounts\Acs + * @var Api\Accounts\Ads */ protected $accounts_ads; @@ -103,7 +103,7 @@ class Ads extends Ldap $this->allContactsDN = $this->accountContactsDN = $this->accounts_ads->ads_context(); // get filter for accounts (incl. additional filter from setup) - $this->accountsFilter = $this->accounts_ads->type_filter('u'); + $this->accountsFilter = $this->accounts_ads->type_filter('u', true); if ($ds) { @@ -205,12 +205,6 @@ class Ads extends Ldap $contact['account_id'] = $this->accounts_ads->objectsid2account_id($data['objectsid']); $contact['id'] = $contact['uid'] = $this->accounts_ads->objectguid2str($data['objectguid']); - // ignore system accounts - if ($contact['account_id'] < Api\Accounts\Ads::MIN_ACCOUNT_ID) return false; - - // ignore deactivated or expired accounts - if (!$this->accounts_ads->user_active($data)) return false; - $this->_inetorgperson2egw($contact, $data, 'displayname'); } diff --git a/api/src/Contacts/Ldap.php b/api/src/Contacts/Ldap.php index bcdc42a30d..c72d322277 100644 --- a/api/src/Contacts/Ldap.php +++ b/api/src/Contacts/Ldap.php @@ -15,6 +15,7 @@ namespace EGroupware\Api\Contacts; use EGroupware\Api; +use EGroupware\Api\Ldap\ServerInfo; /** * LDAP Backend for contacts, compatible with vars and parameters of eTemplate's so_sql. @@ -46,7 +47,7 @@ class Ldap var $accountName; /** - * @var object $ldapServerInfo holds the information about the current used ldap server + * @var ServerInfo $ldapServerInfo holds the information about the current used ldap server */ var $ldapServerInfo; @@ -266,6 +267,15 @@ class Ldap */ ); + /** + * Timestamps ldap => egw used in several places + * @var string[] + */ + public $timestamps2egw = [ + 'createtimestamp' => 'created', + 'modifytimestamp' => 'modified', + ]; + /** * additional schema required by one of the above schema * @@ -287,7 +297,7 @@ class Ldap * * @var array values for keys "ldap_contact_context", "ldap_host", "ldap_context" */ - private $ldap_config; + protected $ldap_config; /** * LDAP connection @@ -771,14 +781,11 @@ class Ldap } // search filter for modified date (eg. for CardDAV sync-report) $datefilter = ''; - static $egw2ldap = array( - 'created' => 'createtimestamp', - 'modified' => 'modifytimestamp', - ); foreach($filter as $key => $value) { $matches = null; - if (is_int($key) && preg_match('/^(contact_)?(modified|created)([<=>]+)([0-9]+)$/', $value, $matches)) + if (is_int($key) && preg_match('/^(contact_)?(modified|created)([<=>]+)([0-9]+)$/', $value, $matches) && + ($attr = array_search($matches[2], $this->timestamps2egw))) { $append = ''; if ($matches[3] == '>') @@ -787,7 +794,7 @@ class Ldap $datefilter .= '(!'; $append = ')'; } - $datefilter .= '('.$egw2ldap[$matches[2]].$matches[3].self::_ts2ldap($matches[4]).')'.$append; + $datefilter .= '('.$attr.$matches[3].self::_ts2ldap($matches[4]).')'.$append; } } @@ -881,7 +888,7 @@ class Ldap $colFilter = $this->_colFilter($filter); $ldapFilter = "(&$objectFilter$searchFilter$colFilter$datefilter)"; //error_log(__METHOD__."(".array2string($criteria).", ".array2string($only_keys).", '$order_by', ".array2string($extra_cols).", '$wildcard', '$empty', '$op', ".array2string($start).", ".array2string($filter).") --> ldapFilter='$ldapFilter'"); - if (!($rows = $this->_searchLDAP($searchDN, $ldapFilter, $this->all_attributes, $addressbookType))) + if (!($rows = $this->_searchLDAP($searchDN, $ldapFilter, $this->all_attributes, $addressbookType, [], $order_by, $start))) { return $rows; } @@ -1086,20 +1093,63 @@ class Ldap return $filter; } + /** + * Get value(s) for LDAP_CONTROL_SORTREQUEST + * + * @param ?string $order_by sql order string eg. "contact_email ASC" + * @return array of arrays with values for keys 'attr', 'oid' (caseIgnoreMatch='2.5.13.3') and 'reverse' + * @todo sorting by multiple criteria is supported in LDAP RFC 2891, but - at least with Univention - gives wired results + */ + protected function sort_values($order_by) + { + $values = []; + while (!empty($order_by) && preg_match("/^(contact_)?([^ ]+)( ASC| DESC)?,?/i", $order_by, $matches)) + { + if (($attr = array_search($matches[2], $this->timestamps2egw))) + { + $values[] = [ + 'attr' => $attr, + // use default match 'oid' => '', + 'reverse' => strtoupper($matches[3]) === ' DESC', + ]; + } + else + { + foreach ($this->schema2egw as $mapping) + { + if (isset($mapping[$matches[2]])) + { + $values[] = [ + 'attr' => $mapping[$matches[2]], + 'oid' => '2.5.13.3', // caseIgnoreMatch + 'reverse' => strtoupper($matches[3]) === ' DESC', + ]; + break; + } + } + } + $order_by = substr($order_by, strlen($matches[0])); + if ($values) break; // sorting by multiple criteria gives wired results + } + //error_log(__METHOD__."('$order_by') returning ".json_encode($values)); + return $values; + } + /** * Perform the actual ldap-search, retrieve and convert all entries * * Used be read and search * - * @internal * @param string $_ldapContext * @param string $_filter * @param array $_attributes * @param int $_addressbooktype * @param array $_skipPlugins =null schema-plugins to skip + * @param string $order_by sql order string eg. "contact_email ASC" + * @param null|int|array $start [$start,$offset], on return null, if result sorted and limited by server * @return array/boolean with eGW contacts or false on error */ - function _searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, array $_skipPlugins=null) + function _searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, array $_skipPlugins=null, $order_by=null, &$start=null) { $this->total = 0; @@ -1112,24 +1162,58 @@ class Ldap //error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype)"); + // check if we require sorting and server supports it + $control = []; + if (PHP_VERSION >= 7.3 && !empty($order_by) && is_array($start) && $this->ldapServerInfo->supportedControl(LDAP_CONTROL_SORTREQUEST, LDAP_CONTROL_VLVREQUEST) && + ($sort_values = $this->sort_values($order_by))) + { + [$offset, $num_rows] = $start; + + $control = [ + [ + 'oid' => LDAP_CONTROL_SORTREQUEST, + //'iscritical' => TRUE, + 'value' => $sort_values, + ], + [ + 'oid' => LDAP_CONTROL_VLVREQUEST, + //'iscritical' => TRUE, + 'value' => [ + 'before' => 0, // Return 0 entry before target + 'after' => $num_rows-1, // total-1 + 'offset' => $offset+1, // first = 1, NOT 0! + 'count' => 0, // We have no idea how many entries there are + ] + ] + ]; + } + if($_addressbooktype == self::ALL || $_ldapContext == $this->allContactsDN) { - $result = ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit); + $result = ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit, null, null, $control); } else { - $result = @ldap_list($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit); + $result = @ldap_list($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit, null, null, $control); } if(!$result || !$entries = ldap_get_entries($this->ds, $result)) return array(); + $this->total = $entries['count']; //error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype) result of $entries[count]"); - $this->total = $entries['count']; + // check if given controls succeeded + if ($control && ldap_parse_result($this->ds, $result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) && + (isset($serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count']))) + { + $this->total = $serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count']; + $start = null; // so caller does NOT run it's own limit + } + foreach($entries as $i => $entry) { if (!is_int($i)) continue; // eg. count $contact = array( - 'id' => $entry['uid'][0] ? $entry['uid'][0] : $entry['entryuuid'][0], + 'id' => $entry['uid'][0] ?? $entry['entryuuid'][0], 'tid' => 'n', // the type id for the addressbook ); foreach($entry['objectclass'] as $ii => $objectclass) @@ -1149,11 +1233,7 @@ class Ldap $objectclass2egw = '_'.$objectclass.'2egw'; if (!in_array($objectclass2egw, (array)$_skipPlugins) &&method_exists($this,$objectclass2egw)) { - if (($ret=$this->$objectclass2egw($contact,$entry)) === false) - { - --$this->total; - continue 2; - } + $this->$objectclass2egw($contact,$entry); } } // read binary jpegphoto only for one result == call by read @@ -1181,10 +1261,7 @@ class Ldap $contact['owner'] = 0; $contact['private'] = 0; } - foreach(array( - 'createtimestamp' => 'created', - 'modifytimestamp' => 'modified', - ) as $ldapFieldName => $egwFieldName) + foreach($this->timestamps2egw as $ldapFieldName => $egwFieldName) { if(!empty($entry[$ldapFieldName][0])) { diff --git a/api/src/Ldap.php b/api/src/Ldap.php index f6c1badf10..9b7eeff923 100644 --- a/api/src/Ldap.php +++ b/api/src/Ldap.php @@ -125,18 +125,23 @@ class Ldap * Convert a single ldap result into a associative array * * @param array $ldap array with numerical and associative indexes and count's + * @param int $depth=0 0: single result / ldap_read, 1: multiple results / ldap_search * @return boolean|array with only associative index and no count's or false on error (parm is no array) */ - static function result2array($ldap) + static function result2array($ldap, $depth=0) { if (!is_array($ldap)) return false; $arr = array(); foreach($ldap as $var => $val) { - if (is_int($var) || $var == 'count') continue; + if (is_int($var) && !$depth || $var === 'count') continue; - if (is_array($val) && $val['count'] == 1) + if ($depth && is_array($val)) + { + $arr[$var] = self::result2array($val, $depth-1); + } + elseif (is_array($val) && $val['count'] == 1) { $arr[$var] = $val[0]; } diff --git a/api/src/Ldap/ServerInfo.php b/api/src/Ldap/ServerInfo.php index e4d53dc25e..0d2e13c4d8 100644 --- a/api/src/Ldap/ServerInfo.php +++ b/api/src/Ldap/ServerInfo.php @@ -60,6 +60,11 @@ class ServerInfo */ var $supportedOIDs = array(); + /** + * @var array OIDs of supported controls LDAP_CONTROL_* + */ + var $suportedControl = []; + /** * Name of host * @@ -128,6 +133,28 @@ class ServerInfo $this->supportedObjectClasses = array_flip($_supportedObjectClasses); } + /** + * sets the supported objectclasses + * + * @param array $_supportedControl LDAP_CONTROL_* values + */ + function setSupportedControl(array $_supportedControl) + { + unset($_supportedControl['count']); + $this->suportedControl = $_supportedControl; + } + + /** + * Check if given (multiple) LDAP_CONTROL_* args are (all) supported + * + * @param int $control LDAP_CONTROL_* value(s) + * @return boolean + */ + function supportedControl($control) + { + return count(array_intersect(func_get_args(), $this->suportedControl)) === func_num_args(); + } + /** * sets the version * @@ -163,7 +190,7 @@ class ServerInfo public static function get($ds, $host, $version=3) { $filter='(objectclass=*)'; - $justthese = array('structuralObjectClass','namingContexts','supportedLDAPVersion','subschemaSubentry','vendorname'); + $justthese = array('structuralObjectClass','namingContexts','supportedLDAPVersion','subschemaSubentry','vendorname','supportedControl'); if(($sr = @ldap_read($ds, '', $filter, $justthese))) { if(($info = ldap_get_entries($ds, $sr))) @@ -207,6 +234,11 @@ class ServerInfo $ldapServerInfo->setSubSchemaEntry($subschemasubentry); } + if (!empty($info[0]['supportedcontrol']) && is_array($info[0]['supportedcontrol'])) + { + $ldapServerInfo->setSupportedControl($info[0]['supportedcontrol']); + } + // create list of supported objetclasses if(!empty($subschemasubentry)) {