forked from fa/breadcrumb-the-shire
443 lines
16 KiB
PHP
443 lines
16 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Service\Import\Profile;
|
|
|
|
use MintyPHP\Service\User\UserAccountService;
|
|
|
|
class UserImportProfile implements ImportProfileInterface
|
|
{
|
|
/**
|
|
* @var array<int, string>|null
|
|
*/
|
|
private ?array $allowedLocales = null;
|
|
|
|
public function __construct(
|
|
private readonly UserImportGateway $gateway,
|
|
private readonly UserAccountService $userAccountService
|
|
) {
|
|
}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'users';
|
|
}
|
|
|
|
public function label(): string
|
|
{
|
|
return t('Users');
|
|
}
|
|
|
|
public function allowedTargets(): array
|
|
{
|
|
return [
|
|
'first_name' => t('First name'),
|
|
'last_name' => t('Last name'),
|
|
'email' => t('Email'),
|
|
'job_title' => t('Job title'),
|
|
'profile_description' => t('Profile description'),
|
|
'phone' => t('Phone'),
|
|
'mobile' => t('Mobile'),
|
|
'short_dial' => t('Short dial'),
|
|
'address' => t('Address'),
|
|
'postal_code' => t('Postal code'),
|
|
'city' => t('City'),
|
|
'region' => t('Region'),
|
|
'country' => t('Country'),
|
|
'hire_date' => t('Hire date'),
|
|
'locale' => t('Language'),
|
|
'active' => t('Active'),
|
|
'tenant' => t('Tenant'),
|
|
'role' => t('Role'),
|
|
'department' => t('Department'),
|
|
];
|
|
}
|
|
|
|
public function requiredTargets(): array
|
|
{
|
|
return ['first_name', 'last_name', 'email'];
|
|
}
|
|
|
|
public function autoMapAliases(): array
|
|
{
|
|
return [
|
|
'first_name' => ['first_name', 'firstname', 'vorname', 'given_name'],
|
|
'last_name' => ['last_name', 'lastname', 'nachname', 'surname', 'family_name'],
|
|
'email' => ['email', 'mail', 'e_mail', 'e-mail'],
|
|
'job_title' => ['job_title', 'job', 'title', 'position'],
|
|
'profile_description' => ['profile_description', 'description', 'about', 'bio'],
|
|
'phone' => ['phone', 'telefon', 'business_phone'],
|
|
'mobile' => ['mobile', 'cell', 'mobile_phone', 'handy'],
|
|
'short_dial' => ['short_dial', 'extension', 'ext'],
|
|
'address' => ['address', 'street', 'strasse', 'anschrift'],
|
|
'postal_code' => ['postal_code', 'zip', 'plz'],
|
|
'city' => ['city', 'ort'],
|
|
'region' => ['region', 'state', 'bundesland'],
|
|
'country' => ['country', 'land'],
|
|
'hire_date' => ['hire_date', 'entry_date', 'start_date', 'eintrittsdatum'],
|
|
'locale' => ['locale', 'language', 'lang', 'sprache'],
|
|
'active' => ['active', 'status', 'enabled', 'aktiv'],
|
|
'tenant' => ['tenant', 'tenant_uuid', 'tenant_id', 'mandant', 'mandant_uuid', 'mandant_id'],
|
|
'role' => ['role', 'role_uuid', 'role_id', 'rolle', 'rolle_uuid', 'rolle_id'],
|
|
'department' => ['department', 'department_uuid', 'department_id', 'abteilung', 'abteilung_uuid', 'abteilung_id'],
|
|
];
|
|
}
|
|
|
|
public function supportsAssignments(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function requiresAssignmentPermission(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function validateMappedRow(array $mapped, array $context): array
|
|
{
|
|
$errors = [];
|
|
$normalized = [];
|
|
|
|
$normalized['first_name'] = trim((string) ($mapped['first_name'] ?? ''));
|
|
$normalized['last_name'] = trim((string) ($mapped['last_name'] ?? ''));
|
|
$normalized['email'] = $this->lower(trim((string) ($mapped['email'] ?? '')));
|
|
|
|
if ($normalized['first_name'] === '') {
|
|
$errors[] = ['code' => 'mapping_required_missing', 'message' => t('Required field is empty: first_name')];
|
|
}
|
|
if ($normalized['last_name'] === '') {
|
|
$errors[] = ['code' => 'mapping_required_missing', 'message' => t('Required field is empty: last_name')];
|
|
}
|
|
if ($normalized['email'] === '' || !filter_var($normalized['email'], FILTER_VALIDATE_EMAIL)) {
|
|
$errors[] = ['code' => 'invalid_email'];
|
|
}
|
|
|
|
foreach ([
|
|
'job_title',
|
|
'profile_description',
|
|
'phone',
|
|
'mobile',
|
|
'short_dial',
|
|
'address',
|
|
'postal_code',
|
|
'city',
|
|
'region',
|
|
'country',
|
|
] as $field) {
|
|
$normalized[$field] = trim((string) ($mapped[$field] ?? ''));
|
|
}
|
|
|
|
$normalized['hire_date'] = trim((string) ($mapped['hire_date'] ?? ''));
|
|
if ($normalized['hire_date'] !== '' && !$this->isValidDate($normalized['hire_date'])) {
|
|
$errors[] = ['code' => 'invalid_hire_date'];
|
|
}
|
|
|
|
$normalized['locale'] = $this->lower(trim((string) ($mapped['locale'] ?? '')));
|
|
if ($normalized['locale'] !== '' && !in_array($normalized['locale'], $this->allowedLocales(), true)) {
|
|
$errors[] = ['code' => 'invalid_locale'];
|
|
}
|
|
|
|
$activeRaw = trim((string) ($mapped['active'] ?? ''));
|
|
$active = $this->parseActive($activeRaw);
|
|
if ($activeRaw !== '' && $active === null) {
|
|
$errors[] = ['code' => 'invalid_active'];
|
|
}
|
|
$normalized['active'] = $active ?? 1;
|
|
|
|
$assignmentResult = $this->resolveAssignments($mapped, $context);
|
|
if (!$assignmentResult['ok']) {
|
|
$errors = array_merge($errors, $assignmentResult['errors'] ?? []);
|
|
} else {
|
|
$normalized['tenant_id'] = (int) ($assignmentResult['tenant_id'] ?? 0);
|
|
$normalized['role_id'] = (int) ($assignmentResult['role_id'] ?? 0);
|
|
$normalized['department_id'] = (int) ($assignmentResult['department_id'] ?? 0);
|
|
}
|
|
|
|
return [
|
|
'ok' => !$errors,
|
|
'normalized' => $normalized,
|
|
'errors' => $errors,
|
|
];
|
|
}
|
|
|
|
public function dryRunRow(array $normalized, array $context): array
|
|
{
|
|
$existing = $this->gateway->findUserByEmail((string) ($normalized['email'] ?? ''));
|
|
if ($existing) {
|
|
return ['status' => 'skipped', 'code' => 'email_exists'];
|
|
}
|
|
return ['status' => 'would_create'];
|
|
}
|
|
|
|
public function commitRow(array $normalized, array $context): array
|
|
{
|
|
$existing = $this->gateway->findUserByEmail((string) ($normalized['email'] ?? ''));
|
|
if ($existing) {
|
|
return ['status' => 'skipped', 'code' => 'email_exists'];
|
|
}
|
|
|
|
$password = $this->generatePassword();
|
|
$input = [
|
|
'first_name' => (string) ($normalized['first_name'] ?? ''),
|
|
'last_name' => (string) ($normalized['last_name'] ?? ''),
|
|
'email' => (string) ($normalized['email'] ?? ''),
|
|
'job_title' => (string) ($normalized['job_title'] ?? ''),
|
|
'profile_description' => (string) ($normalized['profile_description'] ?? ''),
|
|
'phone' => (string) ($normalized['phone'] ?? ''),
|
|
'mobile' => (string) ($normalized['mobile'] ?? ''),
|
|
'short_dial' => (string) ($normalized['short_dial'] ?? ''),
|
|
'address' => (string) ($normalized['address'] ?? ''),
|
|
'postal_code' => (string) ($normalized['postal_code'] ?? ''),
|
|
'city' => (string) ($normalized['city'] ?? ''),
|
|
'region' => (string) ($normalized['region'] ?? ''),
|
|
'country' => (string) ($normalized['country'] ?? ''),
|
|
'hire_date' => (string) ($normalized['hire_date'] ?? ''),
|
|
'locale' => (string) ($normalized['locale'] ?? ''),
|
|
'password' => $password,
|
|
'password2' => $password,
|
|
'tenant_ids' => [(int) ($normalized['tenant_id'] ?? 0)],
|
|
'role_ids' => [(int) ($normalized['role_id'] ?? 0)],
|
|
'department_ids' => [(int) ($normalized['department_id'] ?? 0)],
|
|
'primary_tenant_id' => (int) ($normalized['tenant_id'] ?? 0),
|
|
];
|
|
|
|
if ((int) ($normalized['active'] ?? 1) === 1) {
|
|
$input['active'] = 1;
|
|
} else {
|
|
// The admin create flow expects a present-but-null value for inactive state.
|
|
$input['active'] = null;
|
|
}
|
|
|
|
$result = $this->userAccountService->createFromAdmin($input, (int) ($context['current_user_id'] ?? 0));
|
|
if (!($result['ok'] ?? false)) {
|
|
$errors = $result['errors'] ?? [];
|
|
$message = is_array($errors) && isset($errors[0]) ? (string) $errors[0] : t('User can not be registered');
|
|
return ['status' => 'failed', 'code' => 'create_failed', 'message' => $message];
|
|
}
|
|
|
|
return [
|
|
'status' => 'created',
|
|
'uuid' => (string) ($result['uuid'] ?? ''),
|
|
];
|
|
}
|
|
|
|
public function inFileDuplicateKey(array $normalized): ?string
|
|
{
|
|
$email = $this->lower(trim((string) ($normalized['email'] ?? '')));
|
|
return $email !== '' ? 'email:' . $email : null;
|
|
}
|
|
|
|
public function rowIdentifier(array $normalized): string
|
|
{
|
|
return trim((string) ($normalized['email'] ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $mapped
|
|
* @param array<string, mixed> $context
|
|
* @return array{ok:bool, tenant_id?:int, role_id?:int, department_id?:int, errors?:array<int,array{code:string,message?:string}>}
|
|
*/
|
|
private function resolveAssignments(array $mapped, array $context): array
|
|
{
|
|
$errors = [];
|
|
|
|
$tenantRaw = trim((string) ($mapped['tenant'] ?? ''));
|
|
$roleRaw = trim((string) ($mapped['role'] ?? ''));
|
|
$departmentRaw = trim((string) ($mapped['department'] ?? ''));
|
|
$hasAssignmentInput = $tenantRaw !== '' || $roleRaw !== '' || $departmentRaw !== '';
|
|
|
|
if ($hasAssignmentInput && empty($context['can_import_assignments'])) {
|
|
return ['ok' => false, 'errors' => [['code' => 'assignment_permission_required']]];
|
|
}
|
|
|
|
$tenantId = 0;
|
|
$roleId = 0;
|
|
$departmentId = 0;
|
|
$departmentTenantId = 0;
|
|
|
|
if ($tenantRaw !== '') {
|
|
$tenant = $this->resolveTenant($tenantRaw);
|
|
if (!$tenant) {
|
|
$errors[] = ['code' => 'tenant_not_found'];
|
|
} else {
|
|
$tenantId = (int) ($tenant['id'] ?? 0);
|
|
}
|
|
}
|
|
|
|
if ($roleRaw !== '') {
|
|
$role = $this->resolveRole($roleRaw);
|
|
if (!$role) {
|
|
$errors[] = ['code' => 'role_not_found'];
|
|
} else {
|
|
$roleId = (int) ($role['id'] ?? 0);
|
|
}
|
|
}
|
|
|
|
if ($departmentRaw !== '') {
|
|
$department = $this->resolveDepartment($departmentRaw);
|
|
if (!$department) {
|
|
$errors[] = ['code' => 'department_not_found'];
|
|
} else {
|
|
$departmentId = (int) ($department['id'] ?? 0);
|
|
$departmentTenantId = (int) ($department['tenant_id'] ?? 0);
|
|
if ($tenantId > 0 && $departmentTenantId > 0 && $tenantId !== $departmentTenantId) {
|
|
$errors[] = ['code' => 'department_tenant_mismatch'];
|
|
} elseif ($tenantId <= 0 && $departmentTenantId > 0) {
|
|
$tenantId = $departmentTenantId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($tenantId <= 0) {
|
|
$tenantId = (int) ($context['default_tenant_id'] ?? 0);
|
|
if ($tenantId <= 0 || !$this->resolveTenant((string) $tenantId)) {
|
|
$errors[] = ['code' => 'tenant_not_found'];
|
|
}
|
|
}
|
|
if ($roleId <= 0) {
|
|
$roleId = (int) ($context['default_role_id'] ?? 0);
|
|
if ($roleId <= 0 || !$this->resolveRole((string) $roleId)) {
|
|
$errors[] = ['code' => 'role_not_found'];
|
|
}
|
|
}
|
|
if ($departmentId <= 0) {
|
|
$departmentId = (int) ($context['default_department_id'] ?? 0);
|
|
if ($departmentId <= 0) {
|
|
$errors[] = ['code' => 'department_not_found'];
|
|
} else {
|
|
$department = $this->resolveDepartment((string) $departmentId);
|
|
if (!$department) {
|
|
$errors[] = ['code' => 'department_not_found'];
|
|
} else {
|
|
$departmentTenantId = (int) ($department['tenant_id'] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($tenantId > 0 && $departmentTenantId > 0 && $tenantId !== $departmentTenantId) {
|
|
$errors[] = ['code' => 'department_tenant_mismatch'];
|
|
}
|
|
|
|
if ($tenantId > 0 && empty($context['is_global_admin'])) {
|
|
$allowedTenantIds = $context['allowed_tenant_ids'] ?? [];
|
|
if (!in_array($tenantId, $allowedTenantIds, true)) {
|
|
$errors[] = ['code' => 'assignment_out_of_scope'];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'ok' => !$errors,
|
|
'tenant_id' => $tenantId,
|
|
'role_id' => $roleId,
|
|
'department_id' => $departmentId,
|
|
'errors' => $errors,
|
|
];
|
|
}
|
|
|
|
private function resolveTenant(string $value): ?array
|
|
{
|
|
$tenant = null;
|
|
if ($this->isUuid($value)) {
|
|
$tenant = $this->gateway->findTenantByUuid($value);
|
|
} elseif (ctype_digit($value)) {
|
|
$tenant = $this->gateway->findTenantById((int) $value);
|
|
}
|
|
|
|
if (!$tenant) {
|
|
return null;
|
|
}
|
|
return (($tenant['status'] ?? '') === 'active') ? $tenant : null;
|
|
}
|
|
|
|
private function resolveRole(string $value): ?array
|
|
{
|
|
$role = null;
|
|
if ($this->isUuid($value)) {
|
|
$role = $this->gateway->findRoleByUuid($value);
|
|
} elseif (ctype_digit($value)) {
|
|
$role = $this->gateway->findRoleById((int) $value);
|
|
}
|
|
|
|
if (!$role) {
|
|
return null;
|
|
}
|
|
return ((int) ($role['active'] ?? 0) === 1) ? $role : null;
|
|
}
|
|
|
|
private function resolveDepartment(string $value): ?array
|
|
{
|
|
$department = null;
|
|
if ($this->isUuid($value)) {
|
|
$department = $this->gateway->findDepartmentByUuid($value);
|
|
} elseif (ctype_digit($value)) {
|
|
$department = $this->gateway->findDepartmentById((int) $value);
|
|
}
|
|
|
|
if (!$department) {
|
|
return null;
|
|
}
|
|
return ((int) ($department['active'] ?? 0) === 1) ? $department : null;
|
|
}
|
|
|
|
private function isValidDate(string $value): bool
|
|
{
|
|
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
|
|
return $dt && $dt->format('Y-m-d') === $value;
|
|
}
|
|
|
|
private function parseActive(string $value): ?int
|
|
{
|
|
$value = $this->lower(trim($value));
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (in_array($value, ['1', 'true', 'yes', 'active', 'ja', 'aktiv'], true)) {
|
|
return 1;
|
|
}
|
|
if (in_array($value, ['0', 'false', 'no', 'inactive', 'nein', 'inaktiv'], true)) {
|
|
return 0;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function allowedLocales(): array
|
|
{
|
|
if ($this->allowedLocales !== null) {
|
|
return $this->allowedLocales;
|
|
}
|
|
|
|
$locales = defined('APP_LOCALES') && is_array(APP_LOCALES) ? APP_LOCALES : [];
|
|
$locales = array_values(array_filter(array_map(function ($locale) {
|
|
return $this->lower(trim((string) $locale));
|
|
}, $locales), static fn ($locale) => $locale !== ''));
|
|
|
|
$this->allowedLocales = $locales ?: ['de', 'en'];
|
|
return $this->allowedLocales;
|
|
}
|
|
|
|
private function generatePassword(): string
|
|
{
|
|
try {
|
|
return 'imp_' . bin2hex(random_bytes(24)) . 'A!';
|
|
} catch (\Throwable $exception) {
|
|
return 'imp_' . str_replace('.', '', uniqid('', true)) . 'A!';
|
|
}
|
|
}
|
|
|
|
private function isUuid(string $value): bool
|
|
{
|
|
return (bool) preg_match('/^[a-f0-9-]{36}$/i', $value);
|
|
}
|
|
|
|
private function lower(string $value): string
|
|
{
|
|
if (function_exists('mb_strtolower')) {
|
|
return mb_strtolower($value, 'UTF-8');
|
|
}
|
|
return strtolower($value);
|
|
}
|
|
}
|