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 [,] to do so (--usage gives more options). setup de Benötigt eine Aktualisierung auf Version %1! Benutzen Sie --update-header Passwort[,Benutzer] dafür (--usage gibt weitere Optionen) just now setup de nur jetzt +just users setup de nur Benutzer label to display as option on login page setup de Beschriftung zur Anzeige als Option auf der Login Seite languages updated. setup de Sprachen aktualisiert. ldap accounts configuration setup de LDAP-Benutzerkonten Konfiguration @@ -422,6 +431,7 @@ login as user postgres, eg. by using su as root setup de Als Benutzer postgres e login to mysql - setup de mysql aufrufen - loginname needed for domain configuration setup de Benutzername für die Konfiguration der Domain logout setup de Abmelden +logs to setup de loggt nach mail account of %1 migraged setup de Mail Konto von %1 Migriert mail account of %1 migrated setup de Mail Konto von %1 migriert mail domain (for virtual mail manager) setup de Mail Domain (für Virtual Mail Manager) @@ -510,6 +520,7 @@ path of egroupware install directory (default auto-detected) setup de Pfad des E path to user and group files has to be outside of the webservers document-root!!! setup de Pfad zu Benutzer und Gruppen Dateien MUSS AUSSERHALB des Wurzelverzeichnisses (document root) des Webservers sein!!! path to various directories: have to exist and be writeable by the webserver setup de Pfade zu verschiedenen Verzeichnissen: Diese müssen vorhanden sein und vom Webserver beschreibbar pem certificate setup de PEM Zertifikat +periodic import from ads or ldap into egroupware database setup de Periodischer Import von ADS oder LDAP in die EGroupware Datenbank persistent connections setup de Permanente Verbindungen php client setup de PHP Klient php proxy setup de PHP Proxy @@ -616,6 +627,7 @@ smtp server port setup de SMTP Server Port smtp-authentication required setup de SMTP Authentifizierung benötigt some information for the own service provider metadata setup de Einige Information für die Metadaten des eigenen Service Provider / Dienst some or all of its tables are missing setup de Einige oder alle Tabellen fehlen +source (must be configured above) setup de Quelle (muss oberhalb konfiguriert werden) sources deleted/missing setup de Quellen gelöscht/fehlen sql encryption type setup de SQL-Verschlüsselungstyp für das Passwort ssl validation: setup de SSL Validierung: @@ -728,6 +740,7 @@ user for smtp-authentication (leave it empty if no auth required) setup de Benut usernames (comma-separated) which can get vfs root access (beside setup user) setup de Benutzernamen (mit Komma getrennt) die VFS root Zugriff bekommen können (neben dem Setup Benutzer) usernames are casesensitive setup de Benutzername mit Unterscheidung zwischen Groß- und Kleinschreibung users choice setup de Benutzerauswahl +users, groups and memberships setup de Benutzer, Gruppen und Mitgliedschaften usually more annoying.
admins can use admin >> manage accounts or groups to give access to further apps. setup de Normalerweise mehr ärgerlich als nützlich
utf-8 (unicode) setup de utf-8 (Unicode) validation errors setup de Fehler bei der Prüfung der Eingaben @@ -739,10 +752,13 @@ virtual mail manager (login-name includes domain) setup de Virtual Mail Manager warning! setup de Warnung! we can proceed setup de Wir können fortfahren we could not determine the version of %1, please make sure it is at least %2 setup de Wir konnten die Version von %1 nicht ermitteln, bitte stellen Sie sicher das sie mindestens %2 ist. +we strongly recomment to run a db backup before running the import! setup de Wir empfehlen dringend eine Datensicherung zu machen BEVOR der Import ausgeführt wird! we will automatically update your tables/records to %1 setup de Wir werden Ihre Tabellen/Einträge automatisch zu %1 aktualisieren we will now run a series of tests, which may take a few minutes. click the link below to proceed. setup de Wir werden jetzt eine Serie von Tests durchführen. Das kann ein paar Minuten dauern. Klicken sie auf den folgenden Link um Fortzufahren. weekly setup de wöchentlich welcome to the egroupware installation setup de Herzlich willkommen zur EGroupware Installation +what to do in egroupware if an user get deleted? setup de Was soll gemacht werden in EGroupware, wenn ein Benutzer gelöscht wurde? +what to import? setup de Was soll importiert werden? where should egroupware store file content setup de Wo soll EGroupware den Inhalt von Dateien speichern which database type do you want to use with egroupware? setup de Welchen Datenbanktyp wollen Sie mit EGroupware verwenden? will be downloaded once, unless changed. setup de Wird einmalig heruntergeladen, außer bei Änderung. @@ -778,6 +794,7 @@ you dont have tnef or ytnef installed! it is needed to decode winmail.dat attach you have not created your header.inc.php yet!
you can create it now. setup de Sie haben bisher noch keine header.inc.php angelegt!
Sie können sie jetzt anlegen. you have successfully logged out setup de Sie haben sich erfolgreich abgemeldet. you must enter a username for the admin setup de Sie müssen einen Benutzernamen für den Administrator eingeben! +you must save and run an inital import, before the periodic import will start setup de Sie müssen erst speichern UND den Initialen Import ausführen, bevor der periodische Import startet you need to add at least one egroupware domain / database instance. setup de Sie müssen mindestens eine EGroupware Domain / Datenbank Instanz hinzufügen. you need to configure egroupware: setup de Sie müssen EGroupware konfigurieren: you need to fix the above errors, before the configuration file header.inc.php can be written! setup de Sie müssen die obigen Fehler beheben, bevor die Konfigurationsdatei header.inc.php gespeichert werden kann! diff --git a/setup/lang/egw_en.lang b/setup/lang/egw_en.lang index a56126371d..d37e8d58a2 100644 --- a/setup/lang/egw_en.lang +++ b/setup/lang/egw_en.lang @@ -237,6 +237,7 @@ db root password setup en DB root password db root username setup en DB root username db type setup en DB type db user setup en DB user +deactivate user setup en Deactivate user default setup en Recommended default default file system space per user/group ? setup en Default file system space per user/group ? delete setup en Delete @@ -244,6 +245,7 @@ delete all existing accounts from sql database setup en Delete all existing acco delete all existing sql accounts, groups, acls and preferences (normally not necessary)? setup en Delete all existing SQL accounts, groups, ACLs and preferences. Normally not necessary. delete all my tables and data setup en Delete all my tables and data delete all old languages and install new ones setup en Delete all old languages and install new ones +delete user and his data setup en Delete user AND his data deleting tables setup en Deleting tables. demo server setup setup en Demo server setup deny access setup en Deny access @@ -253,6 +255,7 @@ deregistered setup en De-registered details for admin account setup en Details for Admin account developers' table schema toy setup en Developers' Table Schema Toy did not find any valid db support! setup en Did not find any valid DB support! +do not check for deleted user setup en Do NOT check for deleted user do you want persistent connections (higher performance, but consumes more resources) setup en Do you want persistent connections (higher performance, but consumes more resources) do you want to manage homedirectory and loginshell attributes? setup en Do you want to manage home directory and login shell attributes? documentation setup en Documentation @@ -315,6 +318,7 @@ error in group-creation !!! setup en Error in group creation! error listing "dn=%1"! setup en Error listing "dn=%1"! error modifying dn=%1: %2='%3'! setup en Error modifying dn=%1: %2='%3'! error searching "dn=%1" for "%2"! setup en Error searching "dn=%1" for "%2"! +every setup en Every export has been completed! setup en Export has been completed! failed to mount backup directory! setup en Failed to mount Backup directory! failed updating user "%1" dn="%2"! setup en Failed updating user "%1" dn="%2"! @@ -356,6 +360,8 @@ host,{imap | pop3 | imaps | pop3s},[domain],[{standard(default)|vmailmgr = add d host/ip domain controler setup en Host/IP domain controller hostname/ip of database server setup en Hostname/IP of database server hour (0-24) setup en Hour (0-24) +hours at setup en hours at +how frequent should the import run? setup en How frequent should the import run? however the tables are still in the database setup en The tables are still in the database. however, the application is otherwise installed setup en The application is otherwise installed. however, the application may still work setup en The application may still work @@ -380,6 +386,8 @@ image type selection order setup en Image type selection order imap: admin user,password,emailadmin_imap(|_cyrus|_dovecot) setup en IMAP: Admin user,Password,emailadmin_imap(|_cyrus|_dovecot) import has been completed! setup en Import has been completed! include_path need to contain "." - the current directory setup en include_path need to contain "." - the current directory +incremental import setup en Incremental import +initial import setup en Initial import install setup en Install install all setup en Install all install applications setup en Install applications @@ -401,6 +409,7 @@ is in the webservers docroot setup en is in the web servers docroot is not writeable by the webserver setup en is not writeable by the web server it needs upgrading to version %1! use --update-header [,] to do so (--usage gives more options). setup en It needs upgrading to version %1! Use --update-header [,] to do so (--usage gives more options). just now setup en just now +just users setup en just users label to display as option on login page setup en Label to display as option on login page languages updated. setup en Languages updated. ldap accounts configuration setup en LDAP accounts configuration @@ -426,6 +435,7 @@ login as user postgres, eg. by using su as root setup en Login as user postgres, login to mysql - setup en Login to mysql - loginname needed for domain configuration setup en Login name needed for domain configuration logout setup en Logout +logs to setup en logs to mail account of %1 migraged setup en Mail account of %1 migraged mail account of %1 migrated setup en Mail account of %1 migrated mail domain (for virtual mail manager) setup en Mail domain (for Virtual mail manager) @@ -514,6 +524,7 @@ path of egroupware install directory (default auto-detected) setup en Path of EG path to user and group files has to be outside of the webservers document-root!!! setup en Path to user and group files HAS TO BE OUTSIDE of the web servers document-root! path to various directories: have to exist and be writeable by the webserver setup en Path to various directories: have to exist and be writable by the web server pem certificate setup en PEM certificate +periodic import from ads or ldap into egroupware database setup en Periodic import from ADS or LDAP into EGroupware database persistent connections setup en Persistent connections php client setup en PHP client php proxy setup en PHP proxy @@ -620,6 +631,7 @@ smtp server port setup en SMTP server port smtp-authentication required setup en SMTP-authentication required some information for the own service provider metadata setup en Some information for the own Service Provider metadata some or all of its tables are missing setup en Some or all of its tables are missing +source (must be configured above) setup en Source (must be configured above) sources deleted/missing setup en Sources deleted/missing sql encryption type setup en SQL encryption type for passwords ssl validation: setup en SSL validation: @@ -734,6 +746,7 @@ user for smtp-authentication (leave it empty if no auth required) setup en User usernames (comma-separated) which can get vfs root access (beside setup user) setup en User names, comma-separated, which can get VFS root access (beside setup user) usernames are casesensitive setup en User names are case sensitive users choice setup en Users choice +users, groups and memberships setup en users, groups and memberships usually more annoying.
admins can use admin >> manage accounts or groups to give access to further apps. setup en Usually more annoying.
Admins can use Admin >> Manage accounts or groups to give access to further apps. utf-8 (unicode) setup en utf-8 (Unicode) validation errors setup en Validation errors @@ -745,10 +758,13 @@ virtual mail manager (login-name includes domain) setup en Virtual mail manager warning! setup en Warning! we can proceed setup en We can proceed we could not determine the version of %1, please make sure it is at least %2 setup en We could not determine the version of %1, please make sure it is at least %2 +we strongly recomment to run a db backup before running the import! setup en We strongly recomment to run a DB backup BEFORE running the import! we will automatically update your tables/records to %1 setup en We will automatically update your tables/records to %1 we will now run a series of tests, which may take a few minutes. click the link below to proceed. setup en We will now run a series of tests, which may take a few minutes. Click the link below to proceed. weekly setup en weekly welcome to the egroupware installation setup en Welcome to the EGroupware installation +what to do in egroupware if an user get deleted? setup en What to do in EGroupware if an user get deleted? +what to import? setup en What to import? where should egroupware store file content setup en Where should EGroupware store file content which database type do you want to use with egroupware? setup en Which database type do you want to use with EGroupware? will be downloaded once, unless changed. setup en Will be downloaded once, unless changed. @@ -784,6 +800,7 @@ you dont have tnef or ytnef installed! it is needed to decode winmail.dat attach you have not created your header.inc.php yet!
you can create it now. setup en You have not created your header.inc.php yet!
You can create it now. you have successfully logged out setup en You have successfully logged out. you must enter a username for the admin setup en You must enter a username for the admin. +you must save and run an inital import, before the periodic import will start setup en You must save AND run an inital import, before the periodic import will start you need to add at least one egroupware domain / database instance. setup en You need to add at least one EGroupware domain / database instance. you need to configure egroupware: setup en You need to configure EGroupware: you need to fix the above errors, before the configuration file header.inc.php can be written! setup en You need to fix the above errors, before the configuration file header.inc.php can be written! diff --git a/setup/templates/default/config.tpl b/setup/templates/default/config.tpl index 6c1266163b..a324874ddd 100644 --- a/setup/templates/default/config.tpl +++ b/setup/templates/default/config.tpl @@ -449,6 +449,61 @@   + + {lang_Periodic_import_from_ADS_or_LDAP_into_EGroupware_database}: + + + + {lang_Source_(must_be_configured_above)}: + + + + + + {lang_What_to_import?}: + + + + + + {lang_What_to_do_in_EGroupware_if_an_user_get_deleted?}: + + + + + + {lang_How_frequent_should_the_import_run?}: + + {lang_Every} + + {lang_hours_at} + + {lang_logs_to}: {value_files_dir}/setup/account-import.log + + + + {lang_You_must_save_AND_run_an_inital_import,_before_the_periodic_import_will_start}: + + + + {lang_We_strongly_recomment_to_run_a_DB_backup_BEFORE_running_the_import!} + + + + +   + + {lang_If_using_Mail_authentication}: