diff --git a/api/src/Accounts/Import.php b/api/src/Accounts/Import.php
new file mode 100644
index 0000000000..c92a591f83
--- /dev/null
+++ b/api/src/Accounts/Import.php
@@ -0,0 +1,383 @@
+
+ * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ */
+
+namespace EGroupware\Api\Accounts;
+
+use EGroupware\Api;
+
+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
+
+ $filter = [
+ 'owner' => '0',
+ ];
+ if (!$initial_import)
+ {
+ $filter[] = 'modified>='.$GLOBALS['egw_info']['server']['account_import_lastrun'];
+ }
+ $last_modified = null;
+ $start_import = time();
+ $created = $updated = $uptodate = $errors = 0;
+ $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;
+ if ($accounts_sql->save($sql_account, true) > 0)
+ {
+ $logger("Successful created user '$account[account_lid]' (#$account_id)", 'detail');
+ }
+ else
+ {
+ $logger("Error creaing user '$account[account_lid]' (#$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 ($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)
+ {
+ $result = "Created $created and updated $updated users, with $errors errors.";
+ }
+ else
+ {
+ $result = "All users are up-to-date.";
+ }
+ $logger($result, 'info');
+
+ if ($initial_import && self::installAsyncJob())
+ {
+ $logger('Aync job for periodic import installed', 'info');
+ }
+ }
+ catch(\Exception $e) {
+ $logger($e->getMessage(), 'fatal');
+ throw $e;
+ }
+ return [
+ 'created' => $created,
+ 'updated' => $updated,
+ 'uptodate' => $uptodate,
+ 'errors' => $errors,
+ 'result' => $result,
+ ];
+ }
+
+ /**
+ * 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/setup/account_import.php b/setup/account_import.php
index 483402b49f..c8bd682c2d 100644
--- a/setup/account_import.php
+++ b/setup/account_import.php
@@ -2,12 +2,10 @@
/**
* EGroupware Setup - Account import from LDAP (incl. ADS) to SQL
*
- * The migration is done from the account-repository configured for EGroupware!
- *
- * @link http://www.egroupware.org
+ * @link https://www.egroupware.org
* @package setup
- * @author Ralf Becker " . json_encode($contact + $account) . " Successful created user '$account[account_lid]' (#$account_id) Error creaing user '$account[account_lid]' (#$account_id) Successful updated user '$account[account_lid]' (#$account_id): " . json_encode($diff, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . " Error updating user '$account[account_lid]' (#$account_id) User '$account[account_lid]' (#$account_id) already up to date
\n";
- $new;
- }
- else
- {
- echo "
\n";
- $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)
- {
- echo "
\n";
- if (!$new) $new = false;
- }
- else
- {
- echo "
\n";
- $errors++;
- continue;
- }
- }
- else
- {
- echo "
\n";
- }
- }
- 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'];
- echo "Successful created contact for user '$account[account_lid]' (#$account_id)
$str
\n"; + break; + + case 'error': + case 'info': + echo "$str
\n"; + break; + + default: + echo "$str
\n"; + break; } - } - 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 ($created || $updated || $errors) - { - echo "Created $created and updated $updated users, with $errors errors.
"; - } - else - { - echo "All users are up-to-date.
\n"; - } - if (!$errors) - { - echo "Setting new incremental import time to: $str ($last_run)
\n"; - } + }); } catch (\Exception $e) { - echo "".$e->getMessage()."
\n"; http_response_code(500); - exit; + // 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