Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117751456
D3184.1775185792.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
40 KB
Referenced Files
None
Subscribers
None
D3184.1775185792.diff
View Options
diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php
--- a/src/app/Console/Command.php
+++ b/src/app/Console/Command.php
@@ -29,6 +29,31 @@
*/
protected $dangerous = false;
+
+ /**
+ * Shortcut to creating a progress bar of a particular format with a particular message.
+ *
+ * @param int $count Number of progress steps
+ * @param string $message The description
+ *
+ * @return \Symfony\Component\Console\Helper\ProgressBar
+ */
+ protected function createProgressBar($count, $message = null)
+ {
+ $bar = $this->output->createProgressBar($count);
+ $bar->setFormat(
+ '%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% '
+ );
+
+ if ($message) {
+ $bar->setMessage("{$message}...");
+ }
+
+ $bar->start();
+
+ return $bar;
+ }
+
/**
* Find the domain.
*
diff --git a/src/app/Console/Commands/Data/Import/LdifCommand.php b/src/app/Console/Commands/Data/Import/LdifCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/LdifCommand.php
@@ -0,0 +1,904 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\Console\Command;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+class LdifCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:ldif {file} {owner}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Migrate data from an LDIF file';
+
+ /** @var array Aliases email addresses of the owner */
+ protected $aliases = [];
+
+ /** @var ?string LDAP DN of the account owner */
+ protected $ownerDN;
+
+ /** @var array Packages information */
+ protected $packages = [];
+
+ /** @var ?\App\Wallet A wallet of the account owner */
+ protected $wallet;
+
+ /** @var string Temp table name */
+ protected static $table = 'tmp_ldif_import';
+
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ ini_set("memory_limit", "2048M");
+
+ Schema::dropIfExists(self::$table);
+ Schema::create(
+ self::$table,
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->text('dn')->index();
+ $table->string('type')->nullable()->index();
+ $table->text('data')->nullable();
+ $table->text('error')->nullable();
+ }
+ );
+
+ $this->loadFromFile();
+
+ // TODO: Check for errors, print them and abort?
+
+ // Prepare packages/skus information
+ $this->preparePackagesAndSkus();
+
+ // Import the account owner first
+ $this->importOwner();
+
+ // Import domains first
+ $this->importDomains();
+
+ // Import other objects
+ $this->importUsers();
+ $this->importSharedFolders();
+ //$this->importResources();
+ //$this->importGroups();
+ }
+
+ /**
+ * Load data from the LDIF file into the temp table
+ */
+ protected function loadFromFile(): void
+ {
+ $file = $this->argument('file');
+
+ $numLines = \App\Utils::countLines($file);
+
+ $bar = $this->createProgressBar($numLines, "Parsing input file");
+
+ $fh = fopen($file, 'r');
+
+ $inserts = [];
+ $entry = [];
+ $lastAttr = null;
+
+ $insertFunc = function ($limit = 0) use (&$entry, &$inserts) {
+ if (!empty($entry)) {
+ if ($entry = $this->parseLDAPEntry($entry)) {
+ $inserts[] = $entry;
+ }
+ $entry = [];
+ }
+
+ if (count($inserts) > $limit) {
+ DB::table(self::$table)->insert($inserts);
+ $inserts = [];
+ }
+ };
+
+ while (!feof($fh)) {
+ $line = rtrim(fgets($fh));
+
+ $bar->advance();
+
+ if (trim($line) === '' || $line[0] === '#') {
+ continue;
+ }
+
+ if (substr($line, 0, 3) == 'dn:') {
+ $insertFunc(20);
+ $entry['dn'] = strtolower(substr($line, 4));
+ $lastAttr = 'dn';
+ } elseif (substr($line, 0, 1) == ' ') {
+ if (is_array($entry[$lastAttr])) {
+ $elemNum = count($entry[$lastAttr]) - 1;
+ $entry[$lastAttr][$elemNum] .= ltrim($line);
+ } else {
+ $entry[$lastAttr] .= ltrim($line);
+ }
+ } else {
+ list ($attr, $remainder) = explode(':', $line, 2);
+ $attr = strtolower($attr);
+
+ if ($remainder[0] === ':') {
+ $remainder = base64_decode(substr($remainder, 2));
+ } else {
+ $remainder = ltrim($remainder);
+ }
+
+ if (array_key_exists($attr, $entry)) {
+ if (!is_array($entry[$attr])) {
+ $entry[$attr] = [$entry[$attr]];
+ }
+
+ $entry[$attr][] = $remainder;
+ } else {
+ $entry[$attr] = $remainder;
+ }
+
+ $lastAttr = $attr;
+ }
+ }
+
+ $insertFunc();
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import domains from the temp table
+ */
+ protected function importDomains(): void
+ {
+ $domains = DB::table(self::$table)->where('type', 'domain')->whereNull('error')->get();
+
+ $bar = \App\Utils::createProgressBar($this->output, count($domains), "Importing domains");
+
+ foreach ($domains as $_domain) {
+ $bar->advance();
+
+ $data = json_decode($_domain->data);
+
+ $domain = \App\Domain::withTrashed()->where('namespace', $data->namespace)->first();
+
+ if (!$domain) {
+ $domain = \App\Domain::create([
+ 'namespace' => $data->namespace,
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ // status ???
+ ]);
+
+ // Entitlements
+ $domain->assignPackageAndWallet($this->packages['domain'], $this->wallet);
+ }
+
+ // FIXME: We're importing domains as external, but I can imagine that
+ // using existing public domains could be useful, is it?
+ // FIXME: The same as with users and all other objects, should we support "update mode",
+ // i.e. not throw an error if the domain/user/whatever already exists?
+ // Should we 1. update existing objects, 2. skip them, 3. update them?
+
+ if (!empty($data->aliases)) {
+ foreach ($data->aliases as $alias) {
+ $alias = strtolower($alias);
+ $domain = \App\Domain::withTrashed()->where('namespace', $alias)->first();
+
+ if (!$domain) {
+ $domain = \App\Domain::create([
+ 'namespace' => $alias,
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ // status ???
+ ]);
+
+ // Entitlements
+ $domain->assignPackageAndWallet($this->packages['domain'], $this->wallet);
+ }
+ }
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import groups from the temp table
+ */
+ protected function importGroups(): void
+ {
+ $groups = DB::table(self::$table)->where('type', 'group')->whereNull('error')->get();
+
+ $bar = \App\Utils::createProgressBar($this->output, count($groups), "Importing groups");
+
+ foreach ($groups as $_group) {
+ $bar->advance();
+
+ $data = json_decode($_group->data);
+
+ // Collect group member email addresses
+ // TODO: Make sure all members belong to the same account?
+ $members = $this->resolveUserDNs($data->members);
+
+ if (empty($members)) {
+ $this->setImportError($_group->id, "Members resolve to an empty array");
+ continue;
+ }
+
+ $group = \App\Group::withTrashed()->where('email', $data->email)->first();
+
+ if (!$group) {
+ // TODO: Make sure the domain exists
+
+ $group = \App\Group::create([
+ 'name' => $data->name,
+ 'email' => $data->email,
+ 'members' => $members,
+ // status ???
+ ]);
+
+ $group->assignToWallet($this->wallet);
+ } else {
+ $group->setSetting('sender_policy', null);
+ }
+
+ // Sender policy
+ if (!empty($data->sender_policy)) {
+ $group->setSetting('sender_policy', json_encode($data->sender_policy));
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import resources from the temp table
+ */
+ protected function importResources(): void
+ {
+ $resources = DB::table(self::$table)->where('type', 'resource')->whereNull('error')->get();
+
+ $bar = \App\Utils::createProgressBar($this->output, count($resources), "Importing resources");
+
+ foreach ($resources as $_resource) {
+ $bar->advance();
+
+ $data = json_decode($_resource->data);
+
+ // Resource invitation policy
+ if (!empty($data->invitation_policy) && $data->invitation_policy == 'manual') {
+ $members = $this->resolveUserDNs([$data->owner ?? '']);
+
+ if (empty($members)) {
+ $this->setImportError($_resource->id, "Failed to resolve the resource owner");
+ continue;
+ }
+
+ // TODO: Make sure the owner belongs to the same account?
+ $data->invitation_policy = 'manual:' . $members[0];
+ }
+
+ $resource = \App\Resource::withTrashed()->where('email', $data->email)->first();
+
+ if (!$resource) {
+ // TODO: Make sure the domain exists
+
+ $resource = \App\Resource::create([
+ 'name' => $data->name,
+ 'email' => $data->email,
+ // status ???
+ ]);
+
+ $resource->assignToWallet($this->wallet);
+ } else {
+ $resource->setSetting('invitation_policy', null);
+ }
+
+ // Invitation policy
+ if (!empty($data->invitation_policy)) {
+ $resource->setSetting('invitation_policy', $data->invitation_policy);
+ }
+
+ // Target folder
+ // FIXME: Should we keep the old folder or use Kolab4's generated one
+ if (!empty($data->folder)) {
+ $resource->setSetting('folder', $data->folder);
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import shared folders from the temp table
+ */
+ protected function importSharedFolders(): void
+ {
+ $folders = DB::table(self::$table)->where('type', 'sharedFolder')->whereNull('error')->get();
+
+ $bar = \App\Utils::createProgressBar($this->output, count($folders), "Importing shared folders");
+
+ foreach ($folders as $_folder) {
+ $bar->advance();
+
+ $data = json_decode($_folder->data);
+
+ $folder = \App\SharedFolder::withTrashed()->where('email', $data->email)->first();
+
+ if (!$folder) {
+ // TODO: Make sure the domain exists
+
+ $folder = \App\SharedFolder::create([
+ 'name' => $data->name,
+ // FIXME: Should we keep the old email or use Kolab4's generated one?
+ 'email' => $data->email ?? null,
+ 'type' => $data->type ?? 'mail',
+ // status ???
+ ]);
+
+ $folder->assignToWallet($this->wallet);
+ } else {
+ $folder->setSetting('acl', null);
+ }
+
+ // Invitation policy
+ if (!empty($data->acl)) {
+ $folder->setSetting('acl', json_encode($data->acl));
+ }
+
+ // Target folder
+ // FIXME: Should we keep the old folder or use Kolab4's generated one?
+ if (!empty($data->folder)) {
+ $folder->setSetting('folder', $data->folder);
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import users from the temp table
+ */
+ protected function importUsers(): void
+ {
+ $users = DB::table(self::$table)->where('type', 'user')->whereNull('error');
+
+ // Skip the (already imported) account owner
+ if ($this->ownerDN) {
+ $users->whereNotIn('dn', [$this->ownerDN]);
+ }
+
+ // Import aliases of the owner, we got from importOwner() call
+ if (!empty($this->aliases) && $this->wallet) {
+ $this->wallet->owner->setAliases($this->aliases);
+ }
+
+ $users = $users->get();
+
+ $bar = \App\Utils::createProgressBar($this->output, count($users), "Importing users");
+
+ foreach ($users as $_user) {
+ $bar->advance();
+
+ $this->importSingleUser($_user);
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import the account owner (or find it among the existing accounts)
+ */
+ protected function importOwner(): void
+ {
+ if ($this->ownerDN) {
+ // The owner email found in the import data
+ $bar = \App\Utils::createProgressBar($this->output, 1, "Importing account owner");
+
+ $user = DB::table(self::$table)->where('dn', $this->ownerDN)->first();
+ $user = $this->importSingleUser($user);
+
+ $bar->advance();
+ $bar->finish();
+
+ $this->info("DONE");
+ } else {
+ // The owner email not found in the import data, try existsing users
+ $user = $this->getUser($this->argument('owner'));
+
+ if (!$user) {
+ $this->error("Unable to find the specified account owner");
+ exit(1);
+ }
+ }
+
+ $this->wallet = $user->wallets->first();
+ }
+
+ /**
+ * A helper that imports a single user record
+ */
+ protected function importSingleUser($ldap_user)
+ {
+ $data = json_decode($ldap_user->data);
+
+ $user = \App\User::withTrashed()->where('email', $data->email)->first();
+
+ if (!$user) {
+ // TODO: Make sure the domain exists
+
+ $user = \App\User::create([
+ 'email' => $data->email,
+ // status ???
+ ]);
+
+ // Entitlements
+ $user->assignPackageAndWallet($this->packages['user'], $this->wallet ?: $user->wallets()->first());
+
+ if (!empty($data->quota)) {
+ $quota = ceil($data->quota / 1024 / 1024) - $this->packages['quota'];
+ if ($quota > 0) {
+ $user->assignSku($this->packages['storage'], $quota);
+ }
+ }
+ } else {
+ // FIXME: Should that be an error? I can imagine that executing an import
+ // multiple times might be useful, but on the other hand it is
+ // more complicated, so maybe we should just not support that?
+
+ $user->settings()->delete();
+ $user->aliases()->delete();
+
+ // TODO: update quota (storage entitlements)?
+ }
+
+ // User settings
+ if (!empty($data->settings)) {
+ $settings = [];
+ foreach ($data->settings as $key => $value) {
+ $settings[] = [
+ 'user_id' => $user->id,
+ 'key' => $key,
+ 'value' => $value,
+ ];
+ }
+
+ DB::table('user_settings')->insert($settings);
+ }
+
+ // Update password
+ if ($data->password != $user->password_ldap) {
+ \App\User::where('id', $user->id)->update(['password_ldap' => $data->password]);
+ }
+
+ // Import aliases
+ if (!empty($data->aliases)) {
+ // Some users might have alias entry with their main address, remove it
+ $aliases = array_map('strtolower', $data->aliases);
+ $aliases = array_diff(array_unique($aliases), [$user->email]);
+
+ if (!empty($aliases)) {
+ // TODO: Make sure that domains exist
+
+ if (!$this->wallet) {
+ // This is the account owner creation, at this point we likely do not have
+ // domain records yet, save the aliases to be inserted later (in importUsers())
+ $this->aliases = $aliases;
+ } else {
+ $user->setAliases($aliases);
+ }
+ }
+ }
+
+ return $user;
+ }
+
+ /**
+ * Convert LDAP entry into an object supported by the migration tool
+ *
+ * @param array $entry LDAP entry attributes
+ *
+ * @param array Record data for inserting to the temp table
+ */
+ protected function parseLDAPEntry(array $entry): array
+ {
+ $type = null;
+ $data = null;
+ $error = null;
+
+ $ouTypeMap = [
+ 'Shared Folders' => 'sharedfolder',
+ 'Resources' => 'resource',
+ 'Groups' => 'group',
+ 'People' => 'user',
+ 'Domains' => 'domain',
+ ];
+
+ foreach ($ouTypeMap as $ou => $_type) {
+ if (stripos($entry['dn'], ",ou={$ou}")) {
+ $type = $_type;
+ break;
+ }
+ }
+
+ if (!$type) {
+ $error = "Unknown record type";
+ }
+
+ if (empty($error)) {
+ $method = 'parseLDAP' . ucfirst($type);
+ list($data, $error) = $this->{$method}($entry);
+ }
+
+ return [
+ 'dn' => $entry['dn'],
+ 'type' => $type,
+ 'data' => json_encode($data),
+ 'error' => $error,
+ ];
+ }
+
+ /**
+ * Convert LDAP domain data into Kolab4 "format"
+ */
+ protected function parseLDAPDomain($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['associateddomain'])) {
+ $error = "Missing 'associatedDomain' attribute";
+ } elseif (!empty($entry['inetdomainstatus']) && $entry['inetdomainstatus'] == 'deleted') {
+ $error = "Domain deleted";
+ } else {
+ $result['namespace'] = strtolower($this->attrStringValue($entry, 'associateddomain'));
+
+ if (is_array($entry['associateddomain']) && count($entry['associateddomain']) > 1) {
+ $result['aliases'] = array_slice($entry['associateddomain'], 1);
+ }
+
+ // TODO: inetdomainstatus = suspended ???
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP group data into Kolab4 "format"
+ */
+ protected function parseLDAPGroup($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['cn'])) {
+ $error = "Missing 'cn' attribute";
+ } elseif (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } elseif (empty($entry['uniquemember'])) {
+ $error = "Missing 'uniqueMember' attribute";
+ } else {
+ $result['name'] = $this->attrStringValue($entry, 'cn');
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+ $result['members'] = $this->attrArrayValue($entry, 'uniquemember');
+
+ if (!empty($entry['kolaballowsmtpsender'])) {
+ $policy = $this->attrArrayValue($entry, 'kolaballowsmtpsender');
+ $result['sender_policy'] = $this->parseSenderPolicy($policy);
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP resource data into Kolab4 "format"
+ */
+ protected function parseLDAPResource($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['cn'])) {
+ $error = "Missing 'cn' attribute";
+ } elseif (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } else {
+ $result['name'] = $this->attrStringValue($entry, 'cn');
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+
+ if (!empty($entry['kolabfoldertype'])) {
+ $result['type'] = $this->attrStringValue($entry, 'kolabfoldertype');
+ }
+
+ if (!empty($entry['kolabtargetfolder'])) {
+ $result['folder'] = $this->attrStringValue($entry, 'kolabtargetfolder');
+ }
+
+ if (!empty($entry['owner'])) {
+ $result['owner'] = $this->attrStringValue($entry, 'owner');
+ }
+
+ if (!empty($entry['kolabinvitationpolicy'])) {
+ $policy = $this->attrArrayValue($entry, 'kolabinvitationpolicy');
+ $result['invitation_policy'] = $this->parseInvitationPolicy($policy);
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP shared folder data into Kolab4 "format"
+ */
+ protected function parseLDAPSharedFolder($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['cn'])) {
+ $error = "Missing 'cn' attribute";
+ } elseif (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } else {
+ $result['name'] = $this->attrStringValue($entry, 'cn');
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+
+ if (!empty($entry['kolabfoldertype'])) {
+ $result['type'] = $this->attrStringValue($entry, 'kolabfoldertype');
+ }
+
+ if (!empty($entry['kolabtargetfolder'])) {
+ $result['folder'] = $this->attrStringValue($entry, 'kolabtargetfolder');
+ }
+
+ if (!empty($entry['acl'])) {
+ $result['acl'] = $this->parseACL($this->attrArrayValue($entry, 'acl'));
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP user data into Kolab4 "format"
+ */
+ protected function parseLDAPUser($entry)
+ {
+ $error = null;
+ $result = [];
+
+ $settingAttrs = [
+ 'givenname' => 'first_name',
+ 'sn' => 'last_name',
+ 'telephonenumber' => 'phone',
+ 'mailalternateaddress' => 'external_email',
+ 'mobile' => 'phone',
+ 'o' => 'organization',
+ // 'address' => 'billing_address'
+ ];
+
+ if (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } else {
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+ $result['settings'] = [];
+ $result['aliases'] = [];
+
+ foreach ($settingAttrs as $attr => $setting) {
+ if (!empty($entry[$attr])) {
+ $result['settings'][$setting] = $this->attrStringValue($entry, $attr);
+ }
+ }
+
+ if (!empty($entry['alias'])) {
+ $result['aliases'] = $this->attrArrayValue($entry, 'alias');
+ }
+
+ if (!empty($entry['userpassword'])) {
+ $result['password'] = $this->attrStringValue($entry, 'userpassword');
+ }
+
+ if (!empty($entry['mailquota'])) {
+ $result['quota'] = $this->attrStringValue($entry, 'mailquota');
+ }
+
+ if ($result['email'] == $this->argument('owner')) {
+ $this->ownerDN = $entry['dn'];
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert ldap attribute value to an array
+ */
+ protected static function attrArrayValue($entry, $attribute)
+ {
+ return is_array($entry[$attribute]) ? $entry[$attribute] : [$entry[$attribute]];
+ }
+
+ /**
+ * Convert ldap attribute to a string
+ */
+ protected static function attrStringValue($entry, $attribute)
+ {
+ return is_array($entry[$attribute]) ? $entry[$attribute][0] : $entry[$attribute];
+ }
+
+ /**
+ * Resolve a list of user DNs into email addresses. Makes sure
+ * the returned addresses exist in Kolab4 database.
+ */
+ protected static function resolveUserDNs($user_dns): array
+ {
+ // Get email addresses from the import data
+ $users = DB::table(self::$table)->whereIn('dn', $user_dns)
+ ->where('type', 'user')
+ ->whereNull('error')
+ ->get()
+ ->map(function ($user) {
+ $mdata = json_decode($user->data);
+ return $mdata->email;
+ })
+ ->all();
+
+ // Get email addresses for existing Kolab4 users
+ if (!empty($users)) {
+ $users = \App\User::whereIn('email', $users)->get()->pluck('email')->all();
+ }
+
+ return $users;
+ }
+
+ /**
+ * Validate/convert acl to Kolab4 format
+ */
+ protected static function parseACL(array $acl): array
+ {
+ $map = [
+ 'lrswipkxtecdn' => 'full',
+ 'lrs' => 'read-only',
+ 'read' => 'read-only',
+ 'lrswitedn' => 'read-write',
+ ];
+
+ $supportedRights = ['full', 'read-only', 'read-write'];
+
+ foreach ($acl as $idx => $entry) {
+ $parts = explode(',', $entry);
+ $entry = null;
+
+ if (count($parts) == 2) {
+ $label = trim($parts[0]);
+ $rights = trim($parts[1]);
+ $rights = $map[$rights] ?? $rights;
+
+ if (in_array($rights, $supportedRights) && ($label === 'anyone' || strpos($label, '@'))) {
+ $entry = "{$label}, {$rights}";
+ }
+
+ // TODO: Throw an error or log a warning on unsupported acl entry?
+ }
+
+ $acl[$idx] = $entry;
+ }
+
+ return array_values(array_filter($acl));
+ }
+
+ /**
+ * Validate/convert invitation policy to Kolab4 format
+ */
+ protected static function parseInvitationPolicy(array $policies): ?string
+ {
+ foreach ($policies as $policy) {
+ if ($policy == 'ACT_MANUAL') {
+ // 'owner' attribute handling in another place
+ return 'manual';
+ }
+
+ if ($policy == 'ACT_ACCEPT_AND_NOTIFY') {
+ break; // use the default 'accept' (null) policy
+ }
+
+ if ($policy == 'ACT_REJECT') {
+ return 'reject';
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate/convert sender policy to Kolab4 format
+ */
+ protected static function parseSenderPolicy(array $rules): array
+ {
+ foreach ($rules as $idx => $rule) {
+ $entry = trim($rule);
+ $rule = null;
+
+ // 'deny' rules aren't supported
+ if (isset($entry[0]) && $entry[0] !== '-') {
+ $rule = $entry;
+ }
+
+ $rules[$idx] = $rule;
+ }
+
+ $rules = array_values(array_filter($rules));
+
+ if (!empty($rules) && $rules[count($rules) - 1] != '-') {
+ $rules[] = '-';
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Get/prepare packages/skus information
+ */
+ protected function preparePackagesAndSkus(): void
+ {
+ // Find the tenant
+ if (empty($this->ownerDN)) {
+ if ($user = $this->getUser($this->argument('owner'))) {
+ $tenant_id = $user->tenant_id;
+ }
+ }
+
+ // TODO: Tenant id could be a command option
+
+ if (empty($tenant_id)) {
+ $tenant_id = \config('app.tenant_id');
+ }
+
+ // TODO: We should probably make package titles configurable with command options
+
+ $this->packages = [
+ 'user' => \App\Package::where('title', 'kolab')->where('tenant_id', $tenant_id)->first(),
+ 'domain' => \App\Package::where('title', 'domain-hosting')->where('tenant_id', $tenant_id)->first(),
+ ];
+
+ // Count storage skus
+ $sku = $this->packages['user']->skus()->where('title', 'storage')->first();
+
+ $this->packages['quota'] = $sku ? $sku->pivot->qty : 0;
+ $this->packages['storage'] = \App\Sku::where('title', 'storage')->where('tenant_id', $tenant_id)->first();
+ }
+
+ /**
+ * Set error message for specified import data record
+ */
+ protected static function setImportError($id, $error): void
+ {
+ DB::table(self::$table)->where('id', $id)->update(['error' => $error]);
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -24,6 +24,7 @@
* @property string $email
* @property int $id
* @property string $password
+ * @property string $password_ldap
* @property int $status
* @property int $tenant_id
*/
diff --git a/src/tests/Feature/Console/Data/Import/LdifTest.php b/src/tests/Feature/Console/Data/Import/LdifTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Data/Import/LdifTest.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Tests\Feature\Console\Data\Import;
+
+use Tests\TestCase;
+
+class LdifTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('owner@kolab3.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('owner@kolab3.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ $code = \Artisan::call("data:import:ldif tests/data/kolab3.ldif owner@kolab3.com");
+
+ $this->assertSame(0, $code);
+
+ $owner = \App\User::where('email', 'owner@kolab3.com')->first();
+
+ $this->assertNull($owner->password);
+ $this->assertSame(
+ '{SSHA512}g74+SECTLsM1x0aYkSrTG9sOFzEp8wjCflhshr2DjE7mi1G3iNb4ClH3ljorPRlTgZ105PsQGEpNtNr+XRjigg==',
+ $owner->password_ldap
+ );
+
+ // User settings
+ $this->assertSame('Aleksander', $owner->getSetting('first_name'));
+ $this->assertSame('Machniak', $owner->getSetting('last_name'));
+ $this->assertSame('123456789', $owner->getSetting('phone'));
+ $this->assertSame('external@gmail.com', $owner->getSetting('external_email'));
+ $this->assertSame('Organization AG', $owner->getSetting('organization'));
+
+ // User aliases
+ $aliases = $owner->aliases()->orderBy('alias')->pluck('alias')->all();
+ $this->assertSame(['alias@kolab3-alias.com', 'alias@kolab3.com'], $aliases);
+
+ // Wallet, entitlements
+ $wallet = $owner->wallets->first();
+
+ $this->assertEntitlements($owner, [
+ 'groupware',
+ 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
+
+ // Users
+ $this->assertSame(2, $owner->users(false)->count());
+ $user = $owner->users(false)->where('email', 'user@kolab3.com')->first();
+
+ // User settings
+ $this->assertSame('Jane', $user->getSetting('first_name'));
+ $this->assertSame('Doe', $user->getSetting('last_name'));
+ $this->assertSame('1234567890', $user->getSetting('phone'));
+ $this->assertSame('ext@gmail.com', $user->getSetting('external_email'));
+ $this->assertSame('Org AG', $user->getSetting('organization'));
+
+ // User aliases
+ $aliases = $user->aliases()->orderBy('alias')->pluck('alias')->all();
+ $this->assertSame(['alias2@kolab3.com'], $aliases);
+
+ $this->assertEntitlements($user, [
+ 'groupware',
+ 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
+
+ // Domains
+ $domains = $owner->domains(false, false)->orderBy('namespace')->get();
+
+ $this->assertCount(2, $domains);
+ $this->assertSame('kolab3-alias.com', $domains[0]->namespace);
+ $this->assertSame('kolab3.com', $domains[1]->namespace);
+ $this->assertSame(\App\Domain::TYPE_EXTERNAL, $domains[0]->type);
+ $this->assertSame(\App\Domain::TYPE_EXTERNAL, $domains[1]->type);
+
+ $this->assertEntitlements($domains[0], ['domain-hosting']);
+ $this->assertEntitlements($domains[1], ['domain-hosting']);
+
+ // Shared folders
+ $folders = $owner->sharedFolders(false)->orderBy('email')->get();
+
+ $this->assertCount(2, $folders);
+ $this->assertSame('folder1@kolab3.com', $folders[0]->email);
+ $this->assertSame('folder2@kolab3.com', $folders[1]->email);
+ $this->assertSame('Folder1', $folders[0]->name);
+ $this->assertSame('Folder2', $folders[1]->name);
+ $this->assertSame('mail', $folders[0]->type);
+ $this->assertSame('event', $folders[1]->type);
+ $this->assertSame('["anyone, read-write","owner@kolab3.com, full"]', $folders[0]->getSetting('acl'));
+ $this->assertSame('shared/Folder1@kolab3.com', $folders[0]->getSetting('folder'));
+ $this->assertSame('["anyone, read-only"]', $folders[1]->getSetting('acl'));
+ $this->assertSame('shared/Folder2@kolab3.com', $folders[1]->getSetting('folder'));
+
+ // TODO: Groups, Resources
+
+ // TODO: Error handling
+ }
+
+ /**
+ * Test parseACL() method
+ */
+ public function testParseACL(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $result = $this->invokeMethod($command, 'parseACL', [[]]);
+ $this->assertSame([], $result);
+
+ $acl = [
+ 'anyone, read-write',
+ 'read-only@kolab3.com, read-only',
+ 'read-only@kolab3.com, read',
+ 'full@kolab3.com,full',
+ 'lrswipkxtecdn@kolab3.com, lrswipkxtecdn', // full
+ 'lrs@kolab3.com, lrs', // read-only
+ 'lrswitedn@kolab3.com, lrswitedn', // read-write
+ // unsupported:
+ 'anonymous, read-only',
+ 'group:test, lrs',
+ 'test@kolab3.com, lrspkxtdn',
+ ];
+
+ $expected = [
+ 'anyone, read-write',
+ 'read-only@kolab3.com, read-only',
+ 'read-only@kolab3.com, read-only',
+ 'full@kolab3.com, full',
+ 'lrswipkxtecdn@kolab3.com, full',
+ 'lrs@kolab3.com, read-only',
+ 'lrswitedn@kolab3.com, read-write',
+ ];
+
+ $result = $this->invokeMethod($command, 'parseACL', [$acl]);
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Test parseInvitationPolicy() method
+ */
+ public function testParseInvitationPolicy(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [[]]);
+ $this->assertSame(null, $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['UNKNOWN']]);
+ $this->assertSame(null, $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_ACCEPT']]);
+ $this->assertSame(null, $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_MANUAL']]);
+ $this->assertSame('manual', $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_REJECT']]);
+ $this->assertSame('reject', $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_ACCEPT_AND_NOTIFY', 'ACT_REJECT']]);
+ $this->assertSame(null, $result);
+ }
+
+ /**
+ * Test parseSenderPolicy() method
+ */
+ public function testParseSenderPolicy(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $result = $this->invokeMethod($command, 'parseSenderPolicy', [[]]);
+ $this->assertSame([], $result);
+
+ $result = $this->invokeMethod($command, 'parseSenderPolicy', [['test']]);
+ $this->assertSame(['test', '-'], $result);
+
+ $result = $this->invokeMethod($command, 'parseSenderPolicy', [['test', '-test2', 'test3', '']]);
+ $this->assertSame(['test', 'test3', '-'], $result);
+ }
+}
diff --git a/src/tests/data/kolab3.ldif b/src/tests/data/kolab3.ldif
new file mode 100644
--- /dev/null
+++ b/src/tests/data/kolab3.ldif
@@ -0,0 +1,102 @@
+dn: associateddomain=kolab3.com,ou=Domains,dc=hosted,dc=com
+objectClass: top
+objectClass: domainrelatedobject
+objectClass: inetdomain
+inetDomainBaseDN: ou=kolab3.com,dc=hosted,dc=com
+associatedDomain: kolab3.com
+associatedDomain: kolab3-alias.com
+
+dn: uid=owner@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+cn: Aleksander Machniak
+displayName: Machniak, Aleksander
+givenName: Aleksander
+sn: Machniak
+o: Organization AG
+l:: U2llbWlhbm93aWNlIMWabMSFc2tpZQ==
+mobile: 123456789
+c: PL
+objectClass: top
+objectClass: inetorgperson
+objectClass: kolabinetorgperson
+objectClass: organizationalperson
+objectClass: mailrecipient
+objectClass: country
+objectClass: person
+mail: owner@kolab3.com
+alias: alias@kolab3.com
+alias: alias@kolab3-alias.com
+mailAlternateAddress: external@gmail.com
+mailHost: imap.hosted.com
+mailQuota: 8388608
+uid: owner@kolab3.com
+userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
+nsUniqueID: 229dc10c-1b6a11f7-b7c1edc1-0e0f46c4
+createtimestamp: 20170407081419Z
+modifytimestamp: 20200915082359Z
+
+dn: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+cn: Jane Doe
+displayName: Doe, Jane
+givenName: Jane
+sn: Doe
+o: Org AG
+telephoneNumber: 1234567890
+objectClass: top
+objectClass: inetorgperson
+objectClass: kolabinetorgperson
+objectClass: organizationalperson
+objectClass: mailrecipient
+objectClass: country
+objectClass: person
+mail: user@kolab3.com
+alias: alias2@kolab3.com
+mailAlternateAddress: ext@gmail.com
+mailHost: imap.hosted.com
+mailQuota: 2097152
+uid: user@kolab3.com
+userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
+nsUniqueID: 229dc20c-1b6a11f7-b7c1edc1-0e0f46c4
+
+dn: cn=Group,ou=Groups,ou=kolab3.com,dc=hosted,dc=com
+cn: Group
+mail: group@kolab3.com
+objectClass: top
+objectClass: groupofuniquenames
+objectClass: kolabgroupofuniquenames
+uniqueMember: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+kolabAllowSMTPRecipient: recipient@kolab.org
+kolabAllowSMTPSender: sender@gmail.com
+
+dn: cn=Resource,ou=Resources,ou=kolab3.com,dc=hosted,dc=com
+cn: Resource
+mail: resource-car-resource@kolab3.com
+objectClass: top
+objectClass: kolabsharedfolder
+objectClass: kolabresource
+objectClass: mailrecipient
+owner: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+kolabAllowSMTPRecipient: recipient@kolab.org
+kolabAllowSMTPSender: sender@gmail.com
+kolabInvitationPolicy: ACT_MANUAL
+kolabTargetFolder: shared/Resource@kolab3.com
+
+dn: cn=Folder1,ou=Shared Folders,ou=kolab3.com,dc=hosted,dc=com
+cn: Folder1
+objectClass: kolabsharedfolder
+objectClass: mailrecipient
+objectClass: top
+kolabFolderType: mail
+kolabTargetFolder: shared/Folder1@kolab3.com
+mail: folder1@kolab3.com
+acl: anyone, read-write
+acl: owner@kolab3.com, full
+
+dn: cn=Folder2,ou=Shared Folders,ou=kolab3.com,dc=hosted,dc=com
+cn: Folder2
+objectClass: kolabsharedfolder
+objectClass: mailrecipient
+objectClass: top
+kolabFolderType: event
+kolabTargetFolder: shared/Folder2@kolab3.com
+mail: folder2@kolab3.com
+acl: anyone, read-only
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 3:09 AM (14 h, 37 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822348
Default Alt Text
D3184.1775185792.diff (40 KB)
Attached To
Mode
D3184: Kolab3 migration tool
Attached
Detach File
Event Timeline