Files
ldap_groups_sync/modules/ldap_courses_sync/src/LdapCoursesSync.php
Quintino A. G. Souza 4a65619c27 Adiciona submódulo ldap_courses_sync para sincronização de cursos via LDAP
Cria o módulo modules/ldap_courses_sync/ seguindo o mesmo padrão do
ldap_research_groups_sync, com bundle `course`, campos field_course_*
e field_user_courses, sincronização de membros via atributo LDAP, e
aba "Courses Sync" no painel unificado.

Também registra `courses` no módulo pai (routing, UnifiedAccessRulesForm
e GlobalAccessRuleForm) para suporte a access rules unificadas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 13:49:47 -03:00

1143 lines
37 KiB
PHP

<?php
namespace Drupal\ldap_courses_sync;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\ldap_servers\LdapBridgeInterface;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* Service to synchronize courses from LDAP to groups.
*/
class LdapCoursesSync {
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* LDAP Bridge.
*
* @var \Drupal\ldap_servers\LdapBridgeInterface
*/
protected $ldapBridge;
/**
* LDAP Query storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $ldapQueryStorage;
/**
* Flag indicating that an authorized LDAP sync is in progress.
*
* Set to TRUE before saving user entities during sync so that
* hook_user_presave() knows to allow changes to protected fields.
* External LDAP sync processes (e.g. ldap_user) can also set this flag
* to signal that their saves are authorized.
*
* @var bool
*/
public static bool $syncing = FALSE;
/**
* Returns whether an authorized LDAP sync is currently running.
*
* @return bool
* TRUE if a sync is in progress, FALSE otherwise.
*/
public static function isSyncing(): bool {
return self::$syncing;
}
/**
* Constructor.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
ConfigFactoryInterface $config_factory,
LoggerChannelFactoryInterface $logger_factory,
LdapBridgeInterface $ldap_bridge
) {
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
$this->logger = $logger_factory->get('ldap_courses_sync');
$this->ldapBridge = $ldap_bridge;
// Check if entity type ldap_query_entity exists
if ($entity_type_manager->hasDefinition('ldap_query_entity')) {
$this->ldapQueryStorage = $entity_type_manager->getStorage('ldap_query_entity');
}
else {
$this->logger->warning('Entity type ldap_query_entity not found. Check if ldap_query module is properly installed.');
$this->ldapQueryStorage = NULL;
}
}
/**
* Synchronizes courses from LDAP to groups.
*/
public function syncCourses() {
$this->logger->info('Starting courses synchronization using configured LDAP query.');
$config = $this->configFactory->get('ldap_courses_sync.settings');
$bundle_id = $this->getBundleId();
$this->logger->info('Configuração atual - Server: @server, Query: @query, Group Type: @bundle', [
'@server' => $config->get('ldap_server_id') ?: 'não configurado',
'@query' => $config->get('ldap_query_id') ?: 'não configurado',
'@bundle' => $bundle_id,
]);
try {
$courses = $this->fetchCoursesUsingQuery();
}
catch (\Exception $e) {
$this->logger->error('Erro durante fetchCoursesUsingQuery: @message', ['@message' => $e->getMessage()]);
throw $e;
}
if (empty($courses)) {
$this->logger->warning('Nenhum curso encontrado no LDAP.');
return;
}
$this->logger->info('Iniciando sincronização de @count cursos', ['@count' => count($courses)]);
$entity_storage = $this->getEntityStorage();
$bundle_field = $this->getBundleField();
$existing_entities = $entity_storage->loadByProperties([$bundle_field => $bundle_id]);
// Cria um mapa de códigos existentes
$existing_codes = [];
foreach ($existing_entities as $entity) {
if ($entity->hasField('field_course_code') && !$entity->get('field_course_code')->isEmpty()) {
$code = $entity->get('field_course_code')->value;
$existing_codes[$code] = $entity;
}
}
$created = 0;
$updated = 0;
foreach ($courses as $course_data) {
$code = $course_data['code'];
$name = $course_data['name'];
$phone = $course_data['phone'];
$mail = $course_data['mail'];
// Campos extras (incluindo referências de usuário)
$extra_fields = [];
foreach ($course_data as $field => $value) {
if (!in_array($field, ['code', 'name', 'phone', 'mail', '_ldap_members'])) {
$extra_fields[$field] = $value;
}
}
$this->logger->info('Processando curso - Código: @code, Nome: @name', [
'@code' => $code,
'@name' => $name,
]);
if (isset($existing_codes[$code])) {
// Atualiza entidade existente
$this->logger->info('Atualizando entidade existente com código: @code', ['@code' => $code]);
$entity = $existing_codes[$code];
$name_field = $this->getNameField();
if ($name_field === 'label') {
$entity->set('label', $name);
}
else {
$entity->setName($name);
}
if ($entity->hasField('field_course_phone')) {
$entity->set('field_course_phone', $phone);
}
if ($entity->hasField('field_course_mail')) {
$entity->set('field_course_mail', $mail);
}
foreach ($extra_fields as $field => $value) {
if ($entity->hasField($field)) {
$entity->set($field, $value);
$this->logger->debug('Campo @field atualizado com valor @value', [
'@field' => $field,
'@value' => $value ?? '',
]);
}
else {
$this->logger->warning('Campo @field não existe na entidade', [
'@field' => $field,
]);
}
}
try {
$entity->save();
$updated++;
$this->logger->info('Entidade atualizada com sucesso: @code', ['@code' => $code]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao atualizar entidade @code: @error', [
'@code' => $code,
'@error' => $e->getMessage(),
]);
}
}
else {
// Cria nova entidade
$this->logger->info('Criando nova entidade com código: @code', ['@code' => $code]);
try {
$bundle_field = $this->getBundleField();
$name_field = $this->getNameField();
$entity_data = [
$bundle_field => $bundle_id,
$name_field => $name,
'field_course_code' => $code,
'field_course_phone' => $phone,
'field_course_mail' => $mail,
'uid' => 1,
];
foreach ($extra_fields as $field => $value) {
$entity_data[$field] = $value;
$this->logger->debug('Campo @field configurado com valor @value', [
'@field' => $field,
'@value' => $value ?? '',
]);
}
$entity = $entity_storage->create($entity_data);
$entity->save();
$created++;
$this->logger->info('Entidade criada com sucesso: @code (ID: @id)', [
'@code' => $code,
'@id' => $entity->id(),
]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao criar entidade @code: @error', [
'@code' => $code,
'@error' => $e->getMessage(),
]);
}
}
}
if ($created > 0 || $updated > 0) {
$this->logger->info('Sincronização concluída com sucesso. Criados: @created, Atualizados: @updated', [
'@created' => $created,
'@updated' => $updated,
]);
}
else {
$this->logger->info('Nenhum curso criado ou atualizado.');
}
// Sincroniza membros dos grupos via atributo LDAP
$this->syncMembersFromLdapAttribute($courses);
}
/**
* Busca entidade de curso pelo código.
*
* @param string $course_code
* Código do curso.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* Entidade grupo ou NULL se não encontrado.
*/
public function getCourseByCode($course_code) {
if (empty($course_code)) {
return NULL;
}
$bundle_id = $this->getBundleId();
$bundle_field = $this->getBundleField();
$entity_storage = $this->getEntityStorage();
$entities = $entity_storage->loadByProperties([
$bundle_field => $bundle_id,
'field_course_code' => $course_code,
]);
return !empty($entities) ? reset($entities) : NULL;
}
/**
* Busca cursos usando a query LDAP configurada.
*
* @return array
* Array de cursos com código e descrição.
*/
protected function fetchCoursesUsingQuery() {
$courses = [];
$this->logger->info('Iniciando fetchCoursesUsingQuery...');
if (!$this->ldapQueryStorage) {
$this->logger->error('Storage de queries LDAP não está disponível. Verifique se o módulo ldap_query está instalado.');
return $courses;
}
$this->logger->info('Storage de queries LDAP está disponível.');
try {
$config = $this->configFactory->get('ldap_courses_sync.settings');
$query_id = $config->get('ldap_query_id') ?: 'course_sync';
$this->logger->info('Tentando carregar query LDAP: @query_id', ['@query_id' => $query_id]);
$query_entity = $this->ldapQueryStorage->load($query_id);
if (!$query_entity) {
$this->logger->error('Query LDAP "@query_id" não encontrada. Configure a query via /admin/config/local-modules/ldap-courses-sync', [
'@query_id' => $query_id,
]);
return $courses;
}
$this->logger->info('Query LDAP carregada com sucesso: @label', ['@label' => $query_entity->label()]);
if (!$query_entity->get('status')) {
$this->logger->error('Query LDAP "@query_id" está desabilitada.', [
'@query_id' => $query_id,
]);
return $courses;
}
$this->logger->info('Usando query LDAP configurada: @query_id (@label)', [
'@query_id' => $query_id,
'@label' => $query_entity->label(),
]);
$this->logger->info('Executando query LDAP...');
try {
$server_id = $query_entity->getServerId();
$base_dns = $query_entity->getProcessedBaseDns();
$filter = $query_entity->getFilter();
$attributes = $query_entity->getProcessedAttributes();
$this->logger->info('Parâmetros da query - Server: @server, Base DN: @base_dn, Filter: @filter, Attributes: @attrs', [
'@server' => $server_id,
'@base_dn' => !empty($base_dns) ? implode(', ', $base_dns) : 'vazio',
'@filter' => $filter,
'@attrs' => !empty($attributes) ? implode(', ', $attributes) : 'vazio',
]);
$this->ldapBridge->setServerById($server_id);
if (!$this->ldapBridge->bind()) {
throw new \Exception('Falha ao conectar ao servidor LDAP');
}
$this->logger->info('Conectado ao servidor LDAP com sucesso');
$all_results = [];
foreach ($base_dns as $base_dn) {
$this->logger->info('Executando busca em Base DN: @base_dn', ['@base_dn' => $base_dn]);
try {
$ldap = $this->ldapBridge->get();
$this->logger->info('Obtida instância LDAP do bridge');
// Garante que o atributo de membros está incluído na query
$member_sync_on = $config->get('member_sync_enabled') ?? TRUE;
if ($member_sync_on) {
$member_attr = $config->get('member_attribute') ?: 'member';
if (!empty($attributes) && !in_array($member_attr, $attributes)) {
$attributes[] = $member_attr;
}
}
$attr_options = [];
if (!empty($attributes)) {
$attr_options = ['filter' => $attributes];
}
$this->logger->info('Criando query Symfony LDAP...');
$query = $ldap->query($base_dn, $filter, $attr_options);
$this->logger->info('Executando query Symfony LDAP...');
$base_results = $query->execute();
$this->logger->info('Query executada. Tipo de resultado: @type', ['@type' => gettype($base_results)]);
if ($base_results instanceof \Traversable) {
$base_results = iterator_to_array($base_results);
}
$this->logger->info('Resultados encontrados: @count', ['@count' => count($base_results)]);
if (!empty($base_results) && is_array($base_results)) {
$all_results = array_merge($all_results, $base_results);
}
}
catch (\Exception $e) {
$this->logger->error('Erro na busca LDAP para Base DN @base_dn: @message', [
'@base_dn' => $base_dn,
'@message' => $e->getMessage(),
]);
}
}
$results = $all_results;
$this->logger->info('Query LDAP executada com sucesso. Total de resultados: @count', ['@count' => count($results)]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao executar query LDAP: @message', ['@message' => $e->getMessage()]);
$this->logger->error('Stack trace: @trace', ['@trace' => $e->getTraceAsString()]);
throw $e;
}
$this->logger->info('Verificando resultados da query...');
if (empty($results)) {
$this->logger->warning('Query LDAP não retornou resultados.');
return $courses;
}
$this->logger->info('Query LDAP retornou @count resultados', ['@count' => count($results)]);
$courses = $this->processLdapResults($results);
$this->logger->info('Processados @count cursos via query LDAP', ['@count' => count($courses)]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao executar query LDAP: @message', ['@message' => $e->getMessage()]);
}
return $courses;
}
/**
* Sincroniza membros dos grupos baseado no código de grupo dos usuários.
*/
public function syncGroupMembers() {
$this->logger->info('Starting group membership synchronization.');
$config = $this->configFactory->get('ldap_courses_sync.settings');
$role_mapping_enabled = $config->get('role_mapping_enabled') ?? FALSE;
$role_mappings = $config->get('role_mappings') ?? [];
if (!$role_mapping_enabled) {
$this->logger->info('Role mapping is disabled. Skipping group membership synchronization.');
return;
}
if (empty($role_mappings)) {
$this->logger->warning('Role mapping is enabled but no mappings are configured. Skipping synchronization.');
return;
}
$user_storage = $this->entityTypeManager->getStorage('user');
$query = $user_storage->getQuery()
->condition('status', 1)
->condition('uid', 0, '>')
->accessCheck(FALSE);
$uids = $query->execute();
if (empty($uids)) {
$this->logger->warning('No active users found.');
return;
}
$users = $user_storage->loadMultiple($uids);
$this->logger->info('Processing @count users for group membership', ['@count' => count($users)]);
$group_storage = $this->entityTypeManager->getStorage('group');
$group_type_id = $this->getBundleId();
$groups = $group_storage->loadByProperties(['type' => $group_type_id]);
$this->logger->info('Found @count groups', ['@count' => count($groups)]);
$added = 0;
$removed = 0;
$role_updated = 0;
$skipped = 0;
$already_member = 0;
$matched = 0;
foreach ($users as $user) {
try {
$username = $user->getAccountName();
$expected_memberships = [];
foreach ($groups as $group) {
$group_role = $this->determineUserGroupRole($user, $group, $role_mapping_enabled, $role_mappings);
if ($group_role !== NULL) {
$expected_memberships[$group->id()] = $group_role;
}
}
if (!empty($expected_memberships)) {
$matched++;
}
foreach ($expected_memberships as $group_id => $expected_role) {
$group = $groups[$group_id];
$membership = $group->getMember($user);
if ($membership) {
$current_roles = $membership->getRoles();
$current_role_ids = array_map(function ($role) {
return $role->id();
}, $current_roles);
if (!in_array($expected_role, $current_role_ids)) {
try {
foreach ($current_role_ids as $role_id) {
if ($role_id !== 'member' && $role_id !== $expected_role) {
$membership->removeRole($role_id);
}
}
if ($expected_role !== 'member' && strpos($expected_role, '-member') === FALSE) {
$membership->addRole($expected_role);
}
$membership->save();
$role_updated++;
$this->logger->info('Updated role for user @username in group @group to @role', [
'@username' => $username,
'@group' => $group->label(),
'@role' => $expected_role,
]);
}
catch (\Exception $e) {
$this->logger->error('Failed to update role for user @username in group @group: @error', [
'@username' => $username,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
}
}
else {
$already_member++;
}
}
else {
try {
$values = [];
if ($expected_role !== 'member' && strpos($expected_role, '-member') === FALSE) {
$values['group_roles'] = [$expected_role];
}
$group->addMember($user, $values);
$added++;
$this->logger->info('Added user @username to group @group with role @role', [
'@username' => $username,
'@group' => $group->label(),
'@role' => $expected_role,
]);
}
catch (\Exception $e) {
$this->logger->error('Failed to add user @username to group @group: @error', [
'@username' => $username,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
$skipped++;
}
}
}
foreach ($groups as $group) {
if (!isset($expected_memberships[$group->id()])) {
$membership = $group->getMember($user);
if ($membership) {
try {
$group->removeMember($user);
$removed++;
$this->logger->info('Removed user @username from group @group', [
'@username' => $username,
'@group' => $group->label(),
]);
}
catch (\Exception $e) {
$this->logger->error('Failed to remove user @username from group @group: @error', [
'@username' => $username,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
}
}
}
}
}
catch (\Exception $e) {
$this->logger->error('Error processing user @uid: @error', [
'@uid' => $user->id(),
'@error' => $e->getMessage(),
]);
$skipped++;
}
}
$this->logger->info('Group membership synchronization completed. Matched: @matched, Added: @added, Already member: @already, Removed: @removed, Role updated: @role_updated, Skipped: @skipped', [
'@matched' => $matched,
'@added' => $added,
'@already' => $already_member,
'@removed' => $removed,
'@role_updated' => $role_updated,
'@skipped' => $skipped,
]);
}
/**
* Sincroniza membros dos cursos usando o atributo member do LDAP.
*
* Para cada curso, lê a lista de DNs do atributo configurado
* (padrão: 'member'), resolve cada DN para um usuário Drupal e gerencia
* as associações de membros do grupo (adiciona e remove conforme necessário).
*
* @param array $courses
* Array de dados dos cursos processados, com chave '_ldap_members'.
*/
public function syncMembersFromLdapAttribute(array $courses) {
$config = $this->configFactory->get('ldap_courses_sync.settings');
if (!($config->get('member_sync_enabled') ?? TRUE)) {
$this->logger->info('Member sync from LDAP attribute is disabled.');
return;
}
$this->logger->info('Starting member synchronization from LDAP attribute.');
$group_storage = $this->entityTypeManager->getStorage('group');
$user_storage = $this->entityTypeManager->getStorage('user');
$bundle_id = $this->getBundleId();
$bundle_field = $this->getBundleField();
$total_added = 0;
$total_removed = 0;
$total_not_found = 0;
foreach ($courses as $course_data) {
$code = $course_data['code'];
$ldap_members = $course_data['_ldap_members'] ?? [];
if (empty($ldap_members)) {
$this->logger->debug('No LDAP members for course @code.', ['@code' => $code]);
}
// Localiza a entidade grupo correspondente
$groups = $group_storage->loadByProperties([
$bundle_field => $bundle_id,
'field_course_code' => $code,
]);
if (empty($groups)) {
$this->logger->warning('Course with code @code not found for member sync.', ['@code' => $code]);
continue;
}
$group = reset($groups);
// Resolve DNs/UIDs LDAP para IDs de usuários Drupal
$expected_uids = [];
foreach ($ldap_members as $member_value) {
$username = $member_value;
// Se parece com um DN (contém '='), extrai o uid
if (strpos($member_value, '=') !== FALSE) {
if (preg_match('/uid=([^,]+)/i', $member_value, $matches)) {
$username = $matches[1];
}
else {
$this->logger->debug('Could not extract uid from DN: @dn', ['@dn' => $member_value]);
continue;
}
}
$uid = $this->getUserIdByUsername($username);
if ($uid) {
$expected_uids[$uid] = TRUE;
}
else {
$this->logger->debug('User @username (from @dn) not found in Drupal.', [
'@username' => $username,
'@dn' => $member_value,
]);
$total_not_found++;
}
}
// Obtém membros atuais do grupo
$current_memberships = $group->getMembers();
$current_uids = [];
foreach ($current_memberships as $membership) {
$member_entity = $membership->getUser();
if ($member_entity) {
$current_uids[$member_entity->id()] = TRUE;
}
}
// Adiciona novos membros
foreach (array_keys($expected_uids) as $uid) {
if (!isset($current_uids[$uid])) {
$user = $user_storage->load($uid);
if ($user) {
try {
$group->addMember($user);
$total_added++;
$this->logger->debug('Added user @uid to course @group.', [
'@uid' => $uid,
'@group' => $group->label(),
]);
// Append group to field_user_courses if not already there.
if ($user->hasField('field_user_courses')) {
$existing = $user->get('field_user_courses')->getValue();
$already_set = FALSE;
foreach ($existing as $ref) {
if ($ref['target_id'] == $group->id()) {
$already_set = TRUE;
break;
}
}
if (!$already_set) {
self::$syncing = TRUE;
try {
$user->get('field_user_courses')->appendItem(['target_id' => $group->id()]);
$user->save();
}
finally {
self::$syncing = FALSE;
}
}
}
}
catch (\Exception $e) {
$this->logger->error('Failed to add user @uid to course @group: @error', [
'@uid' => $uid,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
}
}
}
}
// Remove membros que não estão mais no LDAP
foreach (array_keys($current_uids) as $uid) {
if (!isset($expected_uids[$uid])) {
$user = $user_storage->load($uid);
if ($user) {
try {
$group->removeMember($user);
$total_removed++;
$this->logger->debug('Removed user @uid from course @group.', [
'@uid' => $uid,
'@group' => $group->label(),
]);
// Remove group from field_user_courses if present.
if ($user->hasField('field_user_courses')) {
$existing = $user->get('field_user_courses')->getValue();
$updated = array_values(array_filter($existing, fn($ref) => $ref['target_id'] != $group->id()));
if (count($updated) !== count($existing)) {
self::$syncing = TRUE;
try {
$user->set('field_user_courses', $updated);
$user->save();
}
finally {
self::$syncing = FALSE;
}
}
}
}
catch (\Exception $e) {
$this->logger->error('Failed to remove user @uid from course @group: @error', [
'@uid' => $uid,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
}
}
}
}
}
$this->logger->info('Member sync completed. Added: @added, Removed: @removed, Not found in Drupal: @not_found', [
'@added' => $total_added,
'@removed' => $total_removed,
'@not_found' => $total_not_found,
]);
}
/**
* Determina o papel (role) de um usuário em um grupo baseado nos mapeamentos.
*/
protected function determineUserGroupRole($user, $group, $role_mapping_enabled, $role_mappings) {
if (!$role_mapping_enabled || empty($role_mappings)) {
return NULL;
}
foreach ($role_mappings as $mapping) {
$group_role = $mapping['group_role'] ?? '';
$source = $mapping['source'] ?? 'user_field';
$source_field = $mapping['source_field'] ?? '';
$values = $mapping['values'] ?? [];
$group_field = $mapping['group_field'] ?? '';
if (empty($group_role) || empty($source_field)) {
continue;
}
$user_value = NULL;
if ($source === 'user_field') {
if ($user->hasField($source_field) && !$user->get($source_field)->isEmpty()) {
$field = $user->get($source_field);
$field_type = $field->getFieldDefinition()->getType();
if (in_array($field_type, ['entity_reference', 'entity_reference_revisions'])) {
$user_value = $field->target_id;
}
else {
$user_value = $field->value;
}
}
}
elseif ($source === 'ldap_attribute') {
$config = $this->configFactory->get('ldap_courses_sync.settings');
$ldap_user_data = $this->fetchUserLdapAttribute($user, $source_field, $config);
if ($ldap_user_data !== NULL) {
$user_value = $ldap_user_data;
}
}
elseif ($source === 'group_field_match') {
if ($user->hasField($source_field) && !$user->get($source_field)->isEmpty() &&
$group->hasField($group_field) && !$group->get($group_field)->isEmpty()) {
$user_field = $user->get($source_field);
$group_field_obj = $group->get($group_field);
$user_field_type = $user_field->getFieldDefinition()->getType();
if (in_array($user_field_type, ['entity_reference', 'entity_reference_revisions'])) {
$user_value = $user_field->target_id;
}
else {
$user_value = $user_field->value;
}
$group_field_type = $group_field_obj->getFieldDefinition()->getType();
if (in_array($group_field_type, ['entity_reference', 'entity_reference_revisions'])) {
$group_value = $group_field_obj->target_id;
}
else {
$group_value = $group_field_obj->value;
}
if ($user_value !== NULL && $group_value !== NULL &&
strcasecmp(trim($user_value), trim($group_value)) === 0) {
return $group_role;
}
}
continue;
}
if ($user_value !== NULL && !empty($values)) {
foreach ($values as $mapping_value) {
if (strcasecmp(trim($user_value), trim($mapping_value)) === 0) {
return $group_role;
}
}
}
}
return NULL;
}
/**
* Busca um atributo LDAP específico para um usuário.
*/
protected function fetchUserLdapAttribute($user, $attribute, $config) {
try {
$host = $config->get('ldap_host');
$port = $config->get('ldap_port');
$base_dn = $config->get('users_base_dn') ?? $config->get('ldap_base_dn');
$bind_dn = $config->get('ldap_bind_dn');
$bind_password = $config->get('ldap_bind_password');
$users_filter = $config->get('users_filter') ?? '(objectClass=person)';
if (empty($host) || empty($base_dn)) {
return NULL;
}
$ldap_connection = ldap_connect($host, $port);
if (!$ldap_connection) {
return NULL;
}
ldap_set_option($ldap_connection, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap_connection, LDAP_OPT_REFERRALS, 0);
if (!empty($bind_dn) && !empty($bind_password)) {
$bind = @ldap_bind($ldap_connection, $bind_dn, $bind_password);
}
else {
$bind = @ldap_bind($ldap_connection);
}
if (!$bind) {
ldap_close($ldap_connection);
return NULL;
}
$username = $user->getAccountName();
$filter = "(&{$users_filter}(uid={$username}))";
$search = ldap_search($ldap_connection, $base_dn, $filter, [$attribute]);
if (!$search) {
ldap_close($ldap_connection);
return NULL;
}
$entries = ldap_get_entries($ldap_connection, $search);
ldap_close($ldap_connection);
if (!empty($entries) && $entries['count'] > 0 && isset($entries[0][$attribute])) {
return $entries[0][$attribute][0] ?? NULL;
}
return NULL;
}
catch (\Exception $e) {
$this->logger->error('Error fetching LDAP attribute @attribute for user @username: @error', [
'@attribute' => $attribute,
'@username' => $user->getAccountName(),
'@error' => $e->getMessage(),
]);
return NULL;
}
}
/**
* Processes LDAP results using configured attribute mappings.
*
* @param array $results
* Array of LDAP results.
*
* @return array
* Array of processed courses.
*/
protected function processLdapResults(array $results) {
$courses = [];
$config = $this->configFactory->get('ldap_courses_sync.settings');
$attribute_mappings = $config->get('attribute_mappings') ?: [];
$code_mapping = NULL;
foreach ($attribute_mappings as $mapping) {
if ($mapping['field'] === 'field_course_code') {
$code_mapping = $mapping;
break;
}
}
if (!$code_mapping) {
$this->logger->error('No field_course_code mapping found. Cannot process results.');
return $courses;
}
$this->logger->debug('Processing @count LDAP results with @mappings mappings', [
'@count' => count($results),
'@mappings' => count($attribute_mappings),
]);
foreach ($results as $entry) {
$course_data = [];
foreach ($attribute_mappings as $mapping) {
$field = $mapping['field'];
$attribute = $mapping['attribute'];
$mapping_type = $mapping['mapping_type'] ?? 'simple';
$value = NULL;
if ($entry->hasAttribute($attribute)) {
$attr_values = $entry->getAttribute($attribute);
if (is_array($attr_values) && isset($attr_values[0])) {
$value = $attr_values[0];
}
elseif (!empty($attr_values)) {
$value = $attr_values;
}
}
elseif ($mapping_type === 'user_reference') {
$this->logger->warning('Atributo LDAP "@attribute" não encontrado na entrada para o campo "@field". Verifique se o atributo está incluído na LDAP query.', [
'@attribute' => $attribute,
'@field' => $field,
]);
}
if ($mapping_type === 'department_reference') {
if (!empty($value)) {
$dept_id = $this->getDepartmentIdByCode($value);
$course_data[$field] = $dept_id;
}
else {
$course_data[$field] = NULL;
}
}
elseif ($mapping_type === 'user_reference') {
if (!empty($value)) {
$username = $value;
// Se parece com DN (contém '='), tenta extrair o uid= ou o primeiro RDN.
if (strpos($value, '=') !== FALSE) {
if (preg_match('/uid=([^,]+)/i', $value, $matches)) {
$username = $matches[1];
}
else {
// Extrai o valor do primeiro RDN (ex.: cn=João Silva,... → João Silva)
preg_match('/^[^=]+=([^,]+)/i', $value, $matches);
$username = $matches[1] ?? $value;
$this->logger->warning('Campo "@field": DN sem uid= encontrado ("@dn"). Usando primeiro RDN "@username" como username.', [
'@field' => $field,
'@dn' => $value,
'@username' => $username,
]);
}
}
$user_id = $this->getUserIdByUsername($username);
if ($user_id) {
$course_data[$field] = $user_id;
}
else {
$this->logger->warning('Campo "@field": usuário "@username" (valor LDAP: "@value") não encontrado no Drupal. Campo ficará vazio.', [
'@field' => $field,
'@username' => $username,
'@value' => $value,
]);
$course_data[$field] = NULL;
}
}
else {
$course_data[$field] = NULL;
}
}
else {
$course_data[$field] = $value;
}
}
$code = $course_data['field_course_code'] ?? NULL;
if ($code) {
$result = [
'code' => $code,
'name' => $course_data['label'] ?? $course_data['name'] ?? '',
'phone' => $course_data['field_course_phone'] ?? '',
'mail' => $course_data['field_course_mail'] ?? '',
];
// Captura lista de membros do atributo LDAP configurado
$member_attribute = $config->get('member_attribute') ?: 'member';
if ($entry->hasAttribute($member_attribute)) {
$result['_ldap_members'] = $entry->getAttribute($member_attribute) ?? [];
}
else {
$result['_ldap_members'] = [];
}
$basic_course_fields = ['field_course_code', 'label', 'name', 'field_course_phone', 'field_course_mail'];
foreach ($course_data as $key => $value) {
if (!in_array($key, $basic_course_fields)) {
$result[$key] = $value;
}
}
$courses[] = $result;
}
}
return $courses;
}
/**
* Gets Drupal user ID by username.
*/
protected function getUserIdByUsername($username) {
if (empty($username)) {
return NULL;
}
$user_storage = $this->entityTypeManager->getStorage('user');
$users = $user_storage->loadByProperties(['name' => $username]);
if (!empty($users)) {
$user = reset($users);
return $user->id();
}
return NULL;
}
/**
* Returns the Drupal group ID for a department with the given code.
*/
protected function getDepartmentIdByCode(string $code): ?int {
if (empty($code)) {
return NULL;
}
$groups = $this->entityTypeManager->getStorage('group')
->loadByProperties(['field_dept_code' => $code]);
if (!empty($groups)) {
return (int) reset($groups)->id();
}
$this->logger->debug('Department with field_dept_code @code not found.', ['@code' => $code]);
return NULL;
}
/**
* Returns the group entity storage.
*/
protected function getEntityStorage() {
return $this->entityTypeManager->getStorage('group');
}
/**
* Returns the group type ID from configuration.
*/
protected function getBundleId() {
$config = $this->configFactory->get('ldap_courses_sync.settings');
return $config->get('group_type_id') ?: 'course';
}
/**
* Returns the name field for groups.
*/
protected function getNameField() {
return 'label';
}
/**
* Returns the bundle field name for groups.
*/
protected function getBundleField() {
return 'type';
}
}