mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 14:41:29 +01:00
* ActiveDirectory: support huge directories by using server-side sorted and limited queries and no caching in session
This commit is contained in:
parent
3c4d46b030
commit
5afe7ddbca
@ -1048,6 +1048,7 @@ class Accounts
|
|||||||
}
|
}
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of how many entries of each app the account has
|
* Get a list of how many entries of each app the account has
|
||||||
*
|
*
|
||||||
@ -1102,6 +1103,7 @@ class Accounts
|
|||||||
|
|
||||||
return $counts;
|
return $counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function get_owner_columns()
|
protected function get_owner_columns()
|
||||||
{
|
{
|
||||||
$owner_columns = array();
|
$owner_columns = array();
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
namespace EGroupware\Api\Accounts;
|
namespace EGroupware\Api\Accounts;
|
||||||
|
|
||||||
use EGroupware\Api;
|
use EGroupware\Api;
|
||||||
|
use EGroupware\Api\Ldap\ServerInfo;
|
||||||
|
|
||||||
require_once EGW_INCLUDE_ROOT.'/vendor/adldap2/adldap2/src/adLDAP.php';
|
require_once EGW_INCLUDE_ROOT.'/vendor/adldap2/adldap2/src/adLDAP.php';
|
||||||
use adLDAPException;
|
use adLDAPException;
|
||||||
@ -90,7 +91,7 @@ class Ads
|
|||||||
protected static $user_attributes = array(
|
protected static $user_attributes = array(
|
||||||
'objectsid', 'samaccounttype', 'samaccountname',
|
'objectsid', 'samaccounttype', 'samaccountname',
|
||||||
'primarygroupid', 'givenname', 'sn', 'mail', 'displayname', 'telephonenumber',
|
'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;
|
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)
|
* Enable extra debug messages via error_log (error always get logged)
|
||||||
*/
|
*/
|
||||||
@ -124,6 +163,8 @@ class Ads
|
|||||||
$this->frontend = $frontend;
|
$this->frontend = $frontend;
|
||||||
|
|
||||||
$this->adldap = self::get_adldap($this->frontend->config);
|
$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]),
|
$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['accountexpires'][0]),
|
||||||
'account_lastpwd_change' => !isset($data['pwdlastset']) ? null : (!$data['pwdlastset'][0] ? 0 :
|
'account_lastpwd_change' => !isset($data['pwdlastset']) ? null : (!$data['pwdlastset'][0] ? 0 :
|
||||||
$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['pwdlastset'][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 :
|
'account_created' => !isset($data['whencreated'][0]) ? null :
|
||||||
self::_when2ts($data['whencreated'][0]),
|
self::_when2ts($data['whencreated'][0]),
|
||||||
'account_modified' => !isset($data['whenchanged'][0]) ? null :
|
'account_modified' => !isset($data['whenchanged'][0]) ? null :
|
||||||
@ -784,7 +827,7 @@ class Ads
|
|||||||
self::convertUnixTimeToWindowsTime($data[$egw]);
|
self::convertUnixTimeToWindowsTime($data[$egw]);
|
||||||
break;
|
break;
|
||||||
case 'account_status':
|
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';
|
$attributes[$adldap] = $data[$egw] == 'A';
|
||||||
break;
|
break;
|
||||||
case 'account_lastpwd_change':
|
case 'account_lastpwd_change':
|
||||||
@ -847,13 +890,14 @@ class Ads
|
|||||||
* 'lid','firstname','lastname','email' - query only the given field for containing $param[query]
|
* '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['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['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,
|
* @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
|
* account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group
|
||||||
*/
|
*/
|
||||||
function search($param)
|
function search($param)
|
||||||
{
|
{
|
||||||
//error_log(__METHOD__.'('.array2string($param).')');
|
//error_log(__METHOD__.'('.json_encode($param).') '.function_backtrace());
|
||||||
$account_search = &$this->cache['account_search'];
|
$account_search = []; // disabled, we have sorted&limited queries now &$this->cache['account_search'];
|
||||||
|
|
||||||
// check if the query is cached
|
// check if the query is cached
|
||||||
$serial = serialize($param);
|
$serial = serialize($param);
|
||||||
@ -876,6 +920,7 @@ class Ads
|
|||||||
}
|
}
|
||||||
else // we need to run the unlimited query
|
else // we need to run the unlimited query
|
||||||
{
|
{
|
||||||
|
$this->total = null;
|
||||||
$query = Api\Ldap::quote(strtolower($param['query']));
|
$query = Api\Ldap::quote(strtolower($param['query']));
|
||||||
|
|
||||||
$accounts = array();
|
$accounts = array();
|
||||||
@ -902,7 +947,7 @@ class Ads
|
|||||||
static $to_ldap = array(
|
static $to_ldap = array(
|
||||||
'firstname' => 'givenname',
|
'firstname' => 'givenname',
|
||||||
'lastname' => 'sn',
|
'lastname' => 'sn',
|
||||||
'lid' => 'uid',
|
'lid' => 'samaccountname',
|
||||||
'email' => 'mail',
|
'email' => 'mail',
|
||||||
);
|
);
|
||||||
$filter = '('.$to_ldap[$param['query_type']].'=*'.$query.'*)';
|
$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']).'))';
|
$membership_filter = '(|(memberOf='.$this->id2name((int)$param['type'], 'account_dn').')(PrimaryGroupId='.abs($param['type']).'))';
|
||||||
$filter = $filter ? "(&$membership_filter$filter)" : $membership_filter;
|
$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);
|
$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']);
|
$account['account_fullname'] = Api\Accounts::format_username($account['account_lid'],$account['account_firstname'],$account['account_lastname'],$account['account_id']);
|
||||||
$accounts[$account_id] = $account;
|
$accounts[$account_id] = $account;
|
||||||
}
|
}
|
||||||
@ -962,7 +1003,7 @@ class Ads
|
|||||||
uasort($sortedAccounts,array($this,'_sort_callback'));
|
uasort($sortedAccounts,array($this,'_sort_callback'));
|
||||||
$account_search[$unl_serial]['data'] = $sortedAccounts;
|
$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
|
// return only the wanted accounts
|
||||||
reset($sortedAccounts);
|
reset($sortedAccounts);
|
||||||
@ -1021,19 +1062,26 @@ class Ads
|
|||||||
* Get LDAP filter for user, groups or both
|
* Get LDAP filter for user, groups or both
|
||||||
*
|
*
|
||||||
* @param string|null $account_type u = user, g = group, default null = try 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
|
* @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)
|
switch ($account_type)
|
||||||
{
|
{
|
||||||
default: // user or groups
|
default: // user or groups
|
||||||
case 'u':
|
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']))
|
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;
|
if ($account_type === 'u') break;
|
||||||
$user_filter = $type_filter;
|
$user_filter = $type_filter;
|
||||||
// fall through
|
// fall through
|
||||||
@ -1052,6 +1100,40 @@ class Ads
|
|||||||
return $type_filter;
|
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
|
* 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 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 $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 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
|
* @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)
|
if (!$attr_filter)
|
||||||
{
|
{
|
||||||
$filter = $this->type_filter($account_type);
|
$filter = $this->type_filter($account_type, $filter_expired);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -1086,33 +1197,31 @@ class Ads
|
|||||||
$filter .= $this->type_filter($account_type).')';
|
$filter .= $this->type_filter($account_type).')';
|
||||||
}
|
}
|
||||||
$sri = ldap_search($ds=$this->ldap_connection(), $context=$this->ads_context(), $filter,
|
$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 (!$sri)
|
||||||
{
|
{
|
||||||
if (self::$debug) error_log(__METHOD__.'('.array2string($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter') returned ".array2string($sri).' trying to reconnect ...');
|
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,
|
$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)))
|
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)
|
foreach($allValues as $key => $data)
|
||||||
{
|
{
|
||||||
if ($key === 'count') continue;
|
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]);
|
$sid = $data['objectsid'] = $this->adldap->utilities()->getTextSID($data['objectsid'][0]);
|
||||||
$rid = self::sid2account_id($sid);
|
$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] =
|
$accounts[($data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT ? '' : '-').$rid] =
|
||||||
$attrs ? $data : Api\Translation::convert($data['samaccountname'][0], 'utf-8');
|
$attrs ? $data : Api\Translation::convert($data['samaccountname'][0], 'utf-8');
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ class Ads extends Ldap
|
|||||||
/**
|
/**
|
||||||
* Accounts ADS object
|
* Accounts ADS object
|
||||||
*
|
*
|
||||||
* @var Api\Accounts\Acs
|
* @var Api\Accounts\Ads
|
||||||
*/
|
*/
|
||||||
protected $accounts_ads;
|
protected $accounts_ads;
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ class Ads extends Ldap
|
|||||||
$this->allContactsDN = $this->accountContactsDN = $this->accounts_ads->ads_context();
|
$this->allContactsDN = $this->accountContactsDN = $this->accounts_ads->ads_context();
|
||||||
|
|
||||||
// get filter for accounts (incl. additional filter from setup)
|
// 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)
|
if ($ds)
|
||||||
{
|
{
|
||||||
@ -205,12 +205,6 @@ class Ads extends Ldap
|
|||||||
$contact['account_id'] = $this->accounts_ads->objectsid2account_id($data['objectsid']);
|
$contact['account_id'] = $this->accounts_ads->objectsid2account_id($data['objectsid']);
|
||||||
$contact['id'] = $contact['uid'] = $this->accounts_ads->objectguid2str($data['objectguid']);
|
$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');
|
$this->_inetorgperson2egw($contact, $data, 'displayname');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
namespace EGroupware\Api\Contacts;
|
namespace EGroupware\Api\Contacts;
|
||||||
|
|
||||||
use EGroupware\Api;
|
use EGroupware\Api;
|
||||||
|
use EGroupware\Api\Ldap\ServerInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LDAP Backend for contacts, compatible with vars and parameters of eTemplate's so_sql.
|
* LDAP Backend for contacts, compatible with vars and parameters of eTemplate's so_sql.
|
||||||
@ -46,7 +47,7 @@ class Ldap
|
|||||||
var $accountName;
|
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;
|
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
|
* 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"
|
* @var array values for keys "ldap_contact_context", "ldap_host", "ldap_context"
|
||||||
*/
|
*/
|
||||||
private $ldap_config;
|
protected $ldap_config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LDAP connection
|
* LDAP connection
|
||||||
@ -771,14 +781,11 @@ class Ldap
|
|||||||
}
|
}
|
||||||
// search filter for modified date (eg. for CardDAV sync-report)
|
// search filter for modified date (eg. for CardDAV sync-report)
|
||||||
$datefilter = '';
|
$datefilter = '';
|
||||||
static $egw2ldap = array(
|
|
||||||
'created' => 'createtimestamp',
|
|
||||||
'modified' => 'modifytimestamp',
|
|
||||||
);
|
|
||||||
foreach($filter as $key => $value)
|
foreach($filter as $key => $value)
|
||||||
{
|
{
|
||||||
$matches = null;
|
$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 = '';
|
$append = '';
|
||||||
if ($matches[3] == '>')
|
if ($matches[3] == '>')
|
||||||
@ -787,7 +794,7 @@ class Ldap
|
|||||||
$datefilter .= '(!';
|
$datefilter .= '(!';
|
||||||
$append = ')';
|
$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);
|
$colFilter = $this->_colFilter($filter);
|
||||||
$ldapFilter = "(&$objectFilter$searchFilter$colFilter$datefilter)";
|
$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'");
|
//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;
|
return $rows;
|
||||||
}
|
}
|
||||||
@ -1086,20 +1093,63 @@ class Ldap
|
|||||||
return $filter;
|
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
|
* Perform the actual ldap-search, retrieve and convert all entries
|
||||||
*
|
*
|
||||||
* Used be read and search
|
* Used be read and search
|
||||||
*
|
*
|
||||||
* @internal
|
|
||||||
* @param string $_ldapContext
|
* @param string $_ldapContext
|
||||||
* @param string $_filter
|
* @param string $_filter
|
||||||
* @param array $_attributes
|
* @param array $_attributes
|
||||||
* @param int $_addressbooktype
|
* @param int $_addressbooktype
|
||||||
* @param array $_skipPlugins =null schema-plugins to skip
|
* @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
|
* @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;
|
$this->total = 0;
|
||||||
|
|
||||||
@ -1112,24 +1162,58 @@ class Ldap
|
|||||||
|
|
||||||
//error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype)");
|
//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)
|
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
|
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();
|
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]");
|
//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)
|
foreach($entries as $i => $entry)
|
||||||
{
|
{
|
||||||
if (!is_int($i)) continue; // eg. count
|
if (!is_int($i)) continue; // eg. count
|
||||||
|
|
||||||
$contact = array(
|
$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
|
'tid' => 'n', // the type id for the addressbook
|
||||||
);
|
);
|
||||||
foreach($entry['objectclass'] as $ii => $objectclass)
|
foreach($entry['objectclass'] as $ii => $objectclass)
|
||||||
@ -1149,11 +1233,7 @@ class Ldap
|
|||||||
$objectclass2egw = '_'.$objectclass.'2egw';
|
$objectclass2egw = '_'.$objectclass.'2egw';
|
||||||
if (!in_array($objectclass2egw, (array)$_skipPlugins) &&method_exists($this,$objectclass2egw))
|
if (!in_array($objectclass2egw, (array)$_skipPlugins) &&method_exists($this,$objectclass2egw))
|
||||||
{
|
{
|
||||||
if (($ret=$this->$objectclass2egw($contact,$entry)) === false)
|
$this->$objectclass2egw($contact,$entry);
|
||||||
{
|
|
||||||
--$this->total;
|
|
||||||
continue 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// read binary jpegphoto only for one result == call by read
|
// read binary jpegphoto only for one result == call by read
|
||||||
@ -1181,10 +1261,7 @@ class Ldap
|
|||||||
$contact['owner'] = 0;
|
$contact['owner'] = 0;
|
||||||
$contact['private'] = 0;
|
$contact['private'] = 0;
|
||||||
}
|
}
|
||||||
foreach(array(
|
foreach($this->timestamps2egw as $ldapFieldName => $egwFieldName)
|
||||||
'createtimestamp' => 'created',
|
|
||||||
'modifytimestamp' => 'modified',
|
|
||||||
) as $ldapFieldName => $egwFieldName)
|
|
||||||
{
|
{
|
||||||
if(!empty($entry[$ldapFieldName][0]))
|
if(!empty($entry[$ldapFieldName][0]))
|
||||||
{
|
{
|
||||||
|
@ -125,18 +125,23 @@ class Ldap
|
|||||||
* Convert a single ldap result into a associative array
|
* Convert a single ldap result into a associative array
|
||||||
*
|
*
|
||||||
* @param array $ldap array with numerical and associative indexes and count's
|
* @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)
|
* @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;
|
if (!is_array($ldap)) return false;
|
||||||
|
|
||||||
$arr = array();
|
$arr = array();
|
||||||
foreach($ldap as $var => $val)
|
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];
|
$arr[$var] = $val[0];
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,11 @@ class ServerInfo
|
|||||||
*/
|
*/
|
||||||
var $supportedOIDs = array();
|
var $supportedOIDs = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array OIDs of supported controls LDAP_CONTROL_*
|
||||||
|
*/
|
||||||
|
var $suportedControl = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of host
|
* Name of host
|
||||||
*
|
*
|
||||||
@ -128,6 +133,28 @@ class ServerInfo
|
|||||||
$this->supportedObjectClasses = array_flip($_supportedObjectClasses);
|
$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
|
* sets the version
|
||||||
*
|
*
|
||||||
@ -163,7 +190,7 @@ class ServerInfo
|
|||||||
public static function get($ds, $host, $version=3)
|
public static function get($ds, $host, $version=3)
|
||||||
{
|
{
|
||||||
$filter='(objectclass=*)';
|
$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(($sr = @ldap_read($ds, '', $filter, $justthese)))
|
||||||
{
|
{
|
||||||
if(($info = ldap_get_entries($ds, $sr)))
|
if(($info = ldap_get_entries($ds, $sr)))
|
||||||
@ -207,6 +234,11 @@ class ServerInfo
|
|||||||
$ldapServerInfo->setSubSchemaEntry($subschemasubentry);
|
$ldapServerInfo->setSubSchemaEntry($subschemasubentry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($info[0]['supportedcontrol']) && is_array($info[0]['supportedcontrol']))
|
||||||
|
{
|
||||||
|
$ldapServerInfo->setSupportedControl($info[0]['supportedcontrol']);
|
||||||
|
}
|
||||||
|
|
||||||
// create list of supported objetclasses
|
// create list of supported objetclasses
|
||||||
if(!empty($subschemasubentry))
|
if(!empty($subschemasubentry))
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user