diff --git a/api/setup/setup.inc.php b/api/setup/setup.inc.php index 61788dfd86..aec301eb90 100644 --- a/api/setup/setup.inc.php +++ b/api/setup/setup.inc.php @@ -70,7 +70,7 @@ $setup_info['api']['hooks']['vfs_rename'] = 'EGroupware\\Api\\Vfs\\Sharing::vfsU $setup_info['api']['hooks']['vfs_rmdir'] = 'EGroupware\\Api\\Vfs\\Sharing::vfsUpdate'; // hook to update SimpleSAMLphp config -$setup_info['api']['hooks']['setup_config'] = \EGroupware\Api\Auth\Saml::class.'::setupConfig'; +$setup_info['api']['hooks']['setup_config'] = [\EGroupware\Api\Auth\Saml::class.'::setupConfig', \EGroupware\Api\Accounts\Import::class.'::setupConfig']; $setup_info['api']['hooks']['login_discovery'] = \EGroupware\Api\Auth\Saml::class.'::discovery'; // installation checks @@ -140,6 +140,3 @@ $setup_info['groupdav']['author'] = $setup_info['groupdav']['maintainer'] = arra $setup_info['groupdav']['license'] = 'GPL'; $setup_info['groupdav']['hooks']['preferences'] = 'EGroupware\\Api\\CalDAV\\Hooks::menus'; $setup_info['groupdav']['hooks']['settings'] = 'EGroupware\\Api\\CalDAV\\Hooks::settings'; - - - diff --git a/api/src/Accounts/Ads.php b/api/src/Accounts/Ads.php index dbfe9424ab..f4ce1ef3df 100644 --- a/api/src/Accounts/Ads.php +++ b/api/src/Accounts/Ads.php @@ -895,6 +895,7 @@ class Ads * @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 + * @param $param['modified'] int if given minimum modification time * @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 */ @@ -944,6 +945,10 @@ class Ads $membership_filter = '(|(memberOf='.$this->id2name((int)$param['type'], 'account_dn').')(PrimaryGroupId='.abs($param['type']).'))'; $filter = $filter ? "(&$membership_filter$filter)" : $membership_filter; } + if (!empty($param['modified'])) + { + $filter = "(&(whenChanged>=".gmdate('YmdHis', $param['modified']).".0Z)$filter)"; + } 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); @@ -972,6 +977,10 @@ class Ads } $filter = "(|(cn=$query)(description=$query))"; } + if (!empty($param['modified'])) + { + $filter = "(&(whenChanged>=".gmdate('YmdHis', $param['modified']).".0Z)$filter)"; + } foreach($this->filter($filter, 'g', self::$group_attributes) as $account_id => $data) { $accounts[$account_id] = $this->_ldap2group($data); diff --git a/api/src/Accounts/Import.php b/api/src/Accounts/Import.php new file mode 100644 index 0000000000..f085684be4 --- /dev/null +++ b/api/src/Accounts/Import.php @@ -0,0 +1,547 @@ + + * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + */ + +namespace EGroupware\Api\Accounts; + +use EGroupware\Api; + +/** + * Account import from LDAP (incl. ADS) to SQL + * + * @todo check that ADS and LDAP update modification time of account, if memberships change + */ +class Import +{ + public function __construct() + { + // if we run from setup, we need to take care of loading db and egw_info/server + if (isset($GLOBALS['egw_setup'])) + { + if (!is_object($GLOBALS['egw_setup']->db)) + { + $GLOBALS['egw_setup']->loaddb(); + } + $GLOBALS['egw_info']['server'] += Api\Config::read('phpgwapi'); + } + } + + /** + * @param bool $initial_import true: initial sync, false: incremental sync + * @param callable|null $logger function($str, $level) level: "debug", "detail", "info", "error" or "fatal" + * @return array with int values for keys 'created', 'updated', 'uptodate', 'errors' and string 'result' + * @throws \Exception also gets logged as level "fatal" + * @throws \InvalidArgumentException if not correctly configured + */ + public function run(bool $initial_import=true, callable $logger=null) + { + try { + if (!isset($logger)) + { + $logger = static function($str, $level){}; + } + + // determine from where we migrate to what + if (!in_array($source = $GLOBALS['egw_info']['server']['account_import_source'], ['ldap', 'ads'])) + { + throw new \InvalidArgumentException("Invalid account_import_source='{$GLOBALS['egw_info']['server']['account_import_source']}'!"); + } + if (!in_array($type = $GLOBALS['egw_info']['server']['account_import_type'], ['users', 'users_groups'])) + { + throw new \InvalidArgumentException("Invalid account_import_type='{$GLOBALS['egw_info']['server']['account_import_type']}'!"); + } + if (!in_array($delete = $GLOBALS['egw_info']['server']['account_import_delete'], ['yes', 'deactivate', 'no'])) + { + throw new \InvalidArgumentException("Invalid account_import_delete='{$GLOBALS['egw_info']['server']['account_import_delete']}'!"); + } + if (!$initial_import && empty($GLOBALS['egw_info']['server']['account_import_lastrun'])) + { + throw new \InvalidArgumentException(lang("You need to run the inital import first!")); + } + + $class = 'EGroupware\\Api\\Contacts\\'.ucfirst($source); + /** @var Api\Contacts\Ldap $contacts */ + $contacts = new $class($GLOBALS['egw_info']['server']); + $contacts_sql = new Api\Contacts\Sql(); + + $class = 'EGroupware\\Api\\Accounts\\'.ucfirst($source); + /** @var Api\Accounts\Ldap $accounts */ + $accounts = new $class(new Api\Accounts(['account_repository' => $source]+$GLOBALS['egw_info']['server'])); + $accounts_sql = new Api\Accounts\Sql(new Api\Accounts(['account_repository' => 'sql']+$GLOBALS['egw_info']['server'])); + Api\Accounts::cache_invalidate(); // to not get any cached data eg. from the wrong backend + + $created = $updated = $uptodate = $errors = $deleted = 0; + if (in_array('groups', explode('_', $type))) + { + [$created, $updated, $uptodate, $errors, $deleted] = $this->groups( + $initial_import ? null : $GLOBALS['egw_info']['server']['account_import_lastrun'], + $accounts, $accounts_sql, $logger, $delete, $groups); + } + + $filter = [ + 'owner' => '0', + ]; + if (!$initial_import) + { + $filter[] = 'modified>='.$GLOBALS['egw_info']['server']['account_import_lastrun']; + } + $last_modified = null; + $start_import = time(); + $cookie = ''; + $start = ['', 5, &$cookie]; // cookie must be a reference! + do + { + foreach ($contacts->search('', false, '', 'account_lid', '', '', 'AND', $start, $filter) as $contact) + { + $new = null; + if (!isset($last_modified) || (int)$last_modified < (int)$contact['modified']) + { + $last_modified = $contact['modified']; + } + $account = $accounts->read($contact['account_id']); + $logger(json_encode($contact + $account), 'debug'); + // check if account exists in sql + if (!($account_id = $accounts_sql->name2id($account['account_lid']))) + { + $sql_account = $account; + // check if account_id is not yet taken by another user or group --> unset it to let DB assign a new one + if ($accounts_sql->read($account['account_id'])) + { + unset($sql_account['account_id']); + } + if (($account_id = $accounts_sql->save($sql_account, true)) > 0) + { + $logger("Successful created user '$account[account_lid]' (#$account[account_id]". + ($account['account_id'] != $account_id ? " as #$account_id" : '').')', 'detail'); + } + else + { + $logger("Error creaing user '$account[account_lid]' (#$account[account_id])", 'error'); + $errors++; + continue; + } + } + elseif ($account_id < 0) + { + throw new \Exception("User '$account[account_lid]' already exists as group!"); + } + elseif (!($sql_account = $accounts_sql->read($account_id))) + { + throw new \Exception("User '$account[account_lid]' (#$account_id) should exist, but not found!"); + } + else + { + // ignore LDAP specific fields, and empty fields + $relevant = array_filter(array_intersect_key($account, $sql_account), static function ($attr) { + return $attr !== null && $attr !== ''; + }); + unset($relevant['person_id']); // is always different as it's the UID, no need to consider + $to_update = $relevant + $sql_account; + // fix accounts without firstname + if (!isset($to_update['account_firstname']) && $to_update['account_lastname'] === $to_update['account_fullname']) + { + $to_update['account_firstname'] = null; + } + if (($diff = array_diff_assoc($to_update, $sql_account))) + { + if ($accounts_sql->save($to_update) > 0) + { + $logger("Successful updated user '$account[account_lid]' (#$account_id): " . + json_encode($diff, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'detail'); + if (!$new) $new = false; + } + else + { + $logger("Error updating user '$account[account_lid]' (#$account_id)", 'error'); + $errors++; + continue; + } + } + else + { + $logger("User '$account[account_lid]' (#$account_id) already up to date", 'debug'); + } + } + if (!($sql_contact = $contacts_sql->read(['account_id' => $account_id]))) + { + $sql_contact = $contact; + unset($sql_contact['id']); // LDAP contact-id is the UID! + if (!$contacts_sql->save($sql_contact)) + { + $sql_contact['id'] = $contacts_sql->data['id']; + $logger("Successful created contact for user '$account[account_lid]' (#$account_id)", 'detail'); + $new = true; + } + else + { + $logger("Error creating contact for user '$account[account_lid]' (#$account_id)", 'error'); + $errors++; + continue; + } + } + else + { + $to_update = array_merge($sql_contact, array_filter($contact, static function ($attr) { + return $attr !== null && $attr !== ''; + })); + $to_update['id'] = $sql_contact['id']; + if (($diff = array_diff_assoc($to_update, $sql_contact))) + { + if ($contacts_sql->save($to_update) === 0) + { + $logger("Successful updated contact data of '$account[account_lid]' (#$account_id): ". + json_encode($diff, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), 'detail'); + if (!$new) $new = false; + } + else + { + $logger("Error updating contact data of '$account[account_lid]' (#$account_id)", 'error'); + ++$errors; + continue; + } + } + else + { + $logger("Contact data of '$account[account_lid]' (#$account_id) already up to date", 'debug'); + } + } + // if requested, also set memberships + if ($type === 'users_groups') + { + // we need to convert the account_id's of memberships, in case we use different ones in SQL + $accounts_sql->set_memberships(array_filter(array_map(static function($account_lid) use ($groups) + { + return array_search($account_lid, $groups); + }, $account['memberships'])), $account_id); + } + if ($new) + { + ++$created; + } + elseif ($new === false) + { + ++$updated; + } + else + { + ++$uptodate; + } + } + } + while ($start[2] !== ''); + + $last_run = max($start_import-1, $last_modified); + Api\Config::save_value('account_import_lastrun', $last_run, 'phpgwapi'); + $str = gmdate('Y-m-d H:i:s', $last_run). ' UTC'; + if (!$errors) + { + $logger("Setting new incremental import time to: $str ($last_run)", 'detail'); + } + if ($created || $updated || $errors || $deleted) + { + $result = "Created $created, updated $updated and deleted $deleted accounts, with $errors errors."; + } + else + { + $result = "All accounts are up-to-date."; + } + $logger($result, 'info'); + + if ($initial_import && self::installAsyncJob()) + { + $logger('Async job for periodic import installed', 'info'); + } + } + catch(\Exception $e) { + $logger($e->getMessage(), 'fatal'); + throw $e; + } + return [ + 'created' => $created, + 'updated' => $updated, + 'uptodate' => $uptodate, + 'errors' => $errors, + 'deleted' => $deleted, + 'result' => $result, + ]; + } + + /** + * Import all groups + * + * We assume we can list all groups without running into memory or timeout issues. + * Groups with identical names as users are skipped, but logged as error. + * + * We can only delete no longer existing groups, if we query all groups! + * So $delete !== 'no', requires $modified === null. + * + * @param Ldap|Ads $accounts + * @param Sql $accounts_sql + * @param callable $logger function($str, $level) level: "debug", "detail", "info", "error" or "fatal" + * @param string $delete what to do with no longer existing groups: "yes": delete incl. data, "deactivate": delete group, "no": do nothing + * @param int|null $modified null: initial import, int: timestamp of last import + * @param array|null &$groups on return all current groups as account_id => account_lid pairs + * @return array with int values [$created, $updated, $uptodate, $errors, $deleted] + */ + protected function groups($modified, object $accounts, Sql $accounts_sql, callable $logger, string $delete, array &$groups=null) + { + // to delete no longer existing groups, we have to query all groups! + if ($delete !== 'no') + { + $modified = null; + } + + // query all groups in SQL + $sql_groups = $groups = []; + foreach($GLOBALS['egw']->db->select(Sql::TABLE, 'account_id,account_lid', ['account_type' => 'g'], __LINE__, __FILE__) as $row) + { + $sql_groups[-$row['account_id']] = $row['account_lid']; + } + // fill groups with existing ones, for incremental sync, as we need to return all groups + if (!empty($modified)) + { + $groups = $sql_groups; + } + + $created = $updated = $uptodate = $errors = $deleted = 0; + foreach($accounts->search(['type' => 'groups', 'modified' => $modified]) as $account_id => $group) + { + $logger(json_encode($group, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'debug'); + + if (!($sql_id = array_search($group['account_lid'], $sql_groups))) + { + if ($accounts_sql->name2id($group['account_lid']) > 0) + { + $logger("Group '$group[account_lid]' already exists as user --> skipped!", 'error'); + $errors++; + continue; + } + // check if the numeric account_id is not yet taken --> unset account_id and let DB create a new one + if ($accounts_sql->read($account_id)) + { + unset($group['account_id']); + } + if (($sql_id = $accounts_sql->save($group, true)) < 0) + { + $logger("Successful created group '$group[account_lid]' (#$account_id".($sql_id != $account_id ? " as #$sql_id" : '').')', 'detail'); + $created++; + } + else + { + $logger("Error creating group '$group[account_lid]' (#$account_id)", 'error'); + $errors++; + } + } + elseif (!($sql_group = $accounts_sql->read($sql_id))) + { + throw new \Exception("Group '$group[account_lid]' (#$sql_id) should exist, but not found!"); + } + else + { + $group['account_id'] = $sql_id; + unset($sql_group['account_fullname'], $sql_group['account_firstname'], $sql_group['account_lastname']); // not stored anywhere + // ignore LDAP specific fields, and empty fields + $relevant = array_filter(array_intersect_key($group, $sql_group), static function ($attr) { + return $attr !== null && $attr !== ''; + }); + $to_update = $relevant + $sql_group; + if (($diff = array_diff_assoc($to_update, $sql_group))) + { + if ($accounts_sql->save($group, true) > 0) + { + $logger("Successful updated group '$group[account_lid]' (#$sql_id): " . + json_encode($diff, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 'detail'); + $updated++; + } + else + { + $logger("Error updating group '$group[account_lid]' (#$sql_id)", 'error'); + $errors++; + } + } + else + { + $logger("Group '$group[account_lid]' (#$sql_id) already up to date", 'debug'); + $uptodate++; + } + // unset the updated groups, so we can delete the ones not returned from LDAP + unset($sql_groups[$sql_id]); + } + $groups[$sql_id] = $group['account_lid']; + } + + // delete the groups not returned from LDAP, groups can NOT be deactivated, we just delete them in the DB + foreach($delete !== 'no' ? $sql_groups : [] as $account_id => $account_lid) + { + static $acl=null; + if ($delete === 'yes') + { + try { + $cmd = new \admin_cmd_delete_account($account_id); + $cmd->run(); + } + catch (\Exception $e) { + $logger("Error deleting no longer existing group '$account_lid' (#$account_id).", 'error'); + $errors++; + } + } + // still run the SQL commands, as an LDAP/ADS system will not run them + if ($accounts_sql->delete($account_id)) + { + if (!isset($acl)) $acl = new Api\Acl(); + $acl->delete_account($account_id); + $logger("Successful deleted no longer existing group '$account_lid' (#$account_id).", 'detail'); + $deleted++; + } + elseif (!isset($e)) + { + $logger("Error deleting no longer existing group '$account_lid' (#$account_id).", 'error'); + $errors++; + } + } + return [$created, $updated, $uptodate, $errors, $deleted]; + } + + /** + * Hook called when setup configuration is being stored: + * - install/removing cron job to periodic import accounts from LDAP/ADS + * + * @param array $location key "newsettings" with reference to changed settings from setup > configuration + * @throws \Exception for errors + */ + public static function setupConfig(array $location) + { + $config =& $location['newsettings']; + + // check if periodic import is configured AND initial sync already done + foreach(['account_import_type', 'account_import_source', 'account_import_frequency'] as $name) + { + if (empty($config[$name])) + { + self::installAsyncJob(); + return; + } + } + if (empty(Api\Config::read('phpgwapi')['account_import_lastrun'])) + { + self::installAsyncJob(); + } + + self::installAsyncJob((float)$config['account_import_frequency'], $config['account_import_time']); + } + + const ASYNC_JOB_ID = 'AccountsImport'; + + /** + * Install async job for periodic import, if configured + * + * @param float $frequency + * @param string|null $time + * @return bool true: job installed, false: job canceled, if it was already installed + */ + protected static function installAsyncJob(float $frequency=0.0, string $time=null) + { + $async = new Api\Asyncservice(); + $async->cancel_timer(self::ASYNC_JOB_ID); + + if (empty($frequency) && !empty($time) && preg_match('/^\d{2}:\d{2}$/', $time)) + { + $frequency = 24; + } + if ($frequency > 0.0) + { + [$hour, $min] = explode(':', $time ?: '00:00'); + $times = ['hour' => (int)$hour, 'min' => (int)$min]; + if ($frequency >= 36) + { + $times['day'] = '*/'.round($frequency/24.0); // 48h => day: */2 + } + elseif ($frequency >= 24) + { + $times['day'] = '*'; + } + elseif ($frequency >= 1) + { + $times['hour'] = round($frequency) == 1 ? '*' : '*/'.round($frequency); + } + elseif ($frequency >= .1) + { + $times = ['min' => '*/'.(5*round(12*$frequency))]; // .1 => */5, .5 => */30 + } + $async->set_timer($times, self::ASYNC_JOB_ID, self::class.'::async'); + + return true; + } + + return false; + } + + const LOG_FILE = 'setup/account-import.log'; + + /** + * Run incremental import via async job + * + * @return void + */ + public static function async() + { + try { + $import = new self(); + $log = $GLOBALS['egw_info']['server']['files_dir'].'/'.self::LOG_FILE; + if (!file_exists($dir=dirname($log)) && !mkdir($dir) || !is_dir($dir) || + !($fp = fopen($log, 'a+'))) + { + $logger = static function($str, $level) + { + if (!in_array($level, ['debug', 'detail'])) + { + error_log(__METHOD__.' '.strtoupper($level).' '.$str); + } + }; + } + else + { + $logger = static function($str, $level) use ($fp) + { + if (!in_array($level, ['debug', 'detail'])) + { + fwrite($fp, date('Y-m-d H:i:s O').' '.strtoupper($level).' '.$str."\n"); + } + }; + } + $logger(date('Y-m-d H:i:s O').' LDAP account import started', 'info'); + $import->run(false, $logger); + $logger(date('Y-m-d H:i:s O').' LDAP account import finished'.(!empty($fp)?"\n":''), 'info'); + } + catch (\InvalidArgumentException $e) { + _egw_log_exception($e); + // disable async job, something is not configured correct + self::installAsyncJob(); + $logger('Async job for periodic import canceled', 'fatal'); + } + catch (\Exception $e) { + _egw_log_exception($e); + } + if (!empty($fp)) fclose($fp); + } + + /** + * Tail the async import log + * + * @return void + * @throws Api\Exception\WrongParameter + * @todo get this working in setup + */ + public function showLog() + { + echo (new Api\Framework\Minimal())->header(['pngfix' => '']); + $tailer = new Api\Json\Tail(self::LOG_FILE); + echo $tailer->show(); + } +} \ No newline at end of file diff --git a/api/src/Accounts/Sql.php b/api/src/Accounts/Sql.php index a9a1783f4c..3188ef5d27 100644 --- a/api/src/Accounts/Sql.php +++ b/api/src/Accounts/Sql.php @@ -188,9 +188,10 @@ class Sql * If no account_id is set in data the account is added and the new id is set in $data. * * @param array $data array with account-data - * @return int/boolean the account_id or false on error + * @param bool $force_create true: do NOT check with frontend, if account exists + * @return int|false the account_id or false on error */ - function save(&$data) + function save(&$data, $force_create=false) { $to_write = $data; unset($to_write['account_passwd']); @@ -207,7 +208,7 @@ class Sql $to_write['account_lastpwd_change'] = time(); } if ($data['mustchangepassword'] == 1) $to_write['account_lastpwd_change']=0; - if (!(int)$data['account_id'] || !$this->id2name($data['account_id'])) + if ($force_create || !(int)$data['account_id'] || !$this->id2name($data['account_id'])) { if ($to_write['account_id'] < 0) $to_write['account_id'] *= -1; @@ -268,7 +269,10 @@ class Sql } /** - * Delete one account, deletes also all acl-entries for that account + * Delete one account + * + * Does NOT delete acl-entries and memberships, use Acl::delete_account($account_id) for that! + * Users need to be deleted via admin_cmd_delete_account, to ensure proper data removal. * * @param int $account_id numeric account_id * @return boolean true on success, false otherwise @@ -277,13 +281,11 @@ class Sql { if (!(int)$account_id) return false; - $contact_id = $this->id2name($account_id,'person_id'); - if (!$this->db->delete($this->table,array('account_id' => abs($account_id)),__LINE__,__FILE__)) { return false; } - if ($contact_id) + if ($account_id > 0 && ($contact_id = $this->id2name($account_id,'person_id'))) { if (!isset($this->contacts)) $this->contacts = new Api\Contacts(); $this->contacts->delete($contact_id,false); // false = allow to delete accounts (!) diff --git a/api/src/Contacts/Ldap.php b/api/src/Contacts/Ldap.php index 4cf04f981b..a24b57f84e 100644 --- a/api/src/Contacts/Ldap.php +++ b/api/src/Contacts/Ldap.php @@ -1196,8 +1196,6 @@ class Ldap */ function _searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, array $_skipPlugins=null, $order_by=null, &$start=null) { - $this->total = 0; - $_attributes[] = 'entryUUID'; $_attributes[] = 'objectClass'; $_attributes[] = 'createTimestamp'; @@ -1214,24 +1212,43 @@ class Ldap { [$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 - ] + $control[] = [ + 'oid' => LDAP_CONTROL_SORTREQUEST, + //'iscritical' => TRUE, + 'value' => $sort_values, + ]; + $control[] = [ + '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 ] ]; } + elseif (PHP_VERSION >= 7.3 && empty($order_by) && + ($start === false || is_array($start) && count($start) === 3) && + $this->ldapServerInfo->supportedControl(LDAP_CONTROL_PAGEDRESULTS)) + { + if ($start === false) + { + $start = [false, 500, '']; + } + $control[] = [ + 'oid' => LDAP_CONTROL_PAGEDRESULTS, + //'iscritical' => TRUE, + 'value' => [ + 'size' => $start[1], + 'cookie' => $start[2], + ], + ]; + } + if (!is_array($start) || count($start) < 3 || $start[2] === '') + { + $this->total = 0; + } if($_addressbooktype == self::ALL || $_ldapContext == $this->allContactsDN) { @@ -1242,15 +1259,21 @@ class Ldap $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']; + $this->total += $entries['count']; //error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype) result of $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']))) + if ($control && ldap_parse_result($this->ds, $result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) { - $this->total = $serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count']; - $start = null; // so caller does NOT run it's own limit + if (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 + } + elseif (isset($serverctrls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) + { + $start[2] = $serverctrls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; + } } foreach($entries as $i => $entry) @@ -1315,6 +1338,19 @@ class Ldap } $contacts[] = $contact; } + + // if we have a non-empty cookie from paged results, continue reading from the server + while (is_array($start) && count($start) === 3 && $start[0] === false && $start[2] !== '') + { + foreach($this->_searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, $_skipPlugins, $order_by, $start) as $contact) + { + $contacts[] = $contact; + } + } + if (is_array($start) && $start[0] === false) + { + $start = false; + } return $contacts; } diff --git a/setup/account_import.php b/setup/account_import.php new file mode 100644 index 0000000000..c8bd682c2d --- /dev/null +++ b/setup/account_import.php @@ -0,0 +1,58 @@ + + * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + */ + +use EGroupware\Api; + +include('./inc/functions.inc.php'); + +// Authorize the user to use setup app and load the database +if (!$GLOBALS['egw_setup']->auth('Config') || $_POST['cancel']) +{ + Header('Location: index.php'); + exit; +} +// Does not return unless user is authorized + +// check CSRF token for POST requests with any content (setup uses empty POST to call its modules!) +if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST) +{ + Api\Csrf::validate($_POST['csrf_token'], __FILE__); +} + +try { + $import = new Api\Accounts\Import(); + if (!empty($_GET['log'])) + { + $import->showLog(); + return; + } + $import->run(!empty($_GET['initial']) && $_GET['initial'] !== 'false', static function($str, $level) + { + switch($level) + { + case 'fatal': + echo "
$str
\n"; + break; + + case 'error': + case 'info': + echo "$str
\n"; + break; + + default: + echo "$str
\n"; + break; + } + }); +} +catch (\Exception $e) { + http_response_code(500); + // message already output through logger above +} \ No newline at end of file diff --git a/setup/lang/egw_de.lang b/setup/lang/egw_de.lang index 2d616fe15d..eaec76afac 100644 --- a/setup/lang/egw_de.lang +++ b/setup/lang/egw_de.lang @@ -236,6 +236,7 @@ db root password setup de Datenbank Root-Passwort db root username setup de Datenbank Root-Benutzername db type setup de Datenbank-Typ db user setup de Datenbank-Benutzer +deactivate user setup de Benutzer deaktivieren default setup de empfohlene Vorgabe default file system space per user/group ? setup de Vorgabewert Festplattenplatz pro Benutzer / Gruppe ? delete setup de Löschen @@ -243,6 +244,7 @@ delete all existing accounts from sql database setup de Lösche alle bestehenden delete all existing sql accounts, groups, acls and preferences (normally not necessary)? setup de Alle existierende SQL-Benutzer, Gruppen, ACLs und Einstellungen löschen (normalerweise nicht nötig)? delete all my tables and data setup de Alle meine Tabellen und Daten löschen delete all old languages and install new ones setup de Alle installierten Sprachen löschen und neu installieren +delete user and his data setup de Benutzer UND seine Daten löschen deleting tables setup de Lösche Tabellen demo server setup setup de Demo Server Setup deny access setup de Zugriff verweigern @@ -252,6 +254,7 @@ deregistered setup de nicht registriert details for admin account setup de Details des Admin-Kontos developers' table schema toy setup de Entwickler Tabellen Schema "Spielzeug" did not find any valid db support! setup de Konnte keine gültige Datenbankunterstützung finden! +do not check for deleted user setup de Nicht auf gelöschte Benutzer prüfen do you want persistent connections (higher performance, but consumes more resources) setup de Wollen Sie eine permanente Datenbankverbindung (höhere Performance, braucht aber mehr Ressourcen) do you want to manage homedirectory and loginshell attributes? setup de Wollen Sie Benutzerverzeichnisse und Login-Shell Attribute verwalten? documentation setup de Dokumentation @@ -313,6 +316,7 @@ error in group-creation !!! setup de Fehler beim Anlegen der Gruppen !!! error listing "dn=%1"! setup de Fehler beim Auflisten von "dn=%1"! error modifying dn=%1: %2='%3'! setup de Fehler beim Ändern von dn=%1: %2='%3'! error searching "dn=%1" for "%2"! setup de Fehler beim Suchen von "dn=%1" nach "%2"! +every setup de Alle export has been completed! setup de Export ist abgeschlossen! failed to mount backup directory! setup de Konnte Datensicherungsverzeichnis nicht mounten! failed updating user "%1" dn="%2"! setup de Konnte Benutzer "%1" dn="%2" nicht aktualisieren! @@ -353,6 +357,8 @@ host,{imap | pop3 | imaps | pop3s},[domain],[{standard(default)|vmailmgr = add d host/ip domain controler setup de Hostname/IP des Domänencontrollers hostname/ip of database server setup de Hostname/IP des Datenbank-Servers hour (0-24) setup de Stunde (0-24) +hours at setup de Stunden um +how frequent should the import run? setup de Wie häufig soll der Import ausgeführt werden? however the tables are still in the database setup de Wie auch immer, die Tabellen sind noch immer in der Datenbank however, the application is otherwise installed setup de Wie auch immer, die Anwendung ist ansonsten installiert however, the application may still work setup de Wie auch immer, die Anwendung mag dennoch funktionieren @@ -376,6 +382,8 @@ if you use only languages of the same charset (eg. western european ones) you do image type selection order setup de Auswahlreihenfolge der Bilddateitypen import has been completed! setup de Import ist beendet! include_path need to contain "." - the current directory setup de include_path muss "." - das aktuelle Verzeichnis - enthalten +incremental import setup de Inkrementeller Import +initial import setup de Initialer Import install setup de Installieren install all setup de Alle Installieren install applications setup de Anwendungen installieren @@ -397,6 +405,7 @@ is in the webservers docroot setup de ist im Dokumentenverzeichnis (Documentroot is not writeable by the webserver setup de ist nicht vom Webserver schreibbar it needs upgrading to version %1! use --update-header