diff --git a/ldap_groups_sync.routing.yml b/ldap_groups_sync.routing.yml index ed5af7b..d59a06a 100644 --- a/ldap_groups_sync.routing.yml +++ b/ldap_groups_sync.routing.yml @@ -23,4 +23,4 @@ ldap_groups_sync.access_rule_form: requirements: _permission: 'administer ldap groups sync' rule_index: 'new|\d+' - group_type: 'departments|research_groups' + group_type: 'departments|research_groups|courses' diff --git a/modules/ldap_courses_sync/config/optional/core.entity_form_display.group.course.default.yml b/modules/ldap_courses_sync/config/optional/core.entity_form_display.group.course.default.yml new file mode 100644 index 0000000..2943e8c --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/core.entity_form_display.group.course.default.yml @@ -0,0 +1,102 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.field.group.course.field_course_code + - field.field.group.course.field_course_coord + - field.field.group.course.field_course_coord_assoc + - field.field.group.course.field_course_mail + - field.field.group.course.field_course_phone + - field.field.group.course.field_course_department + - group.type.course + module: + - path +id: group.course.default +targetEntityType: group +bundle: course +mode: default +content: + field_course_code: + type: string_textfield + weight: 122 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + field_course_mail: + type: email_default + weight: 125 + region: content + settings: + placeholder: '' + size: 60 + third_party_settings: { } + field_course_phone: + type: string_textfield + weight: 124 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + field_course_coord: + type: entity_reference_autocomplete + weight: 126 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + field_course_coord_assoc: + type: entity_reference_autocomplete + weight: 127 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + field_course_department: + type: entity_reference_autocomplete + weight: 25 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + label: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + langcode: + type: language_select + weight: 2 + region: content + settings: + include_locked: true + third_party_settings: { } + path: + type: path + weight: 30 + region: content + settings: { } + third_party_settings: { } + status: + type: boolean_checkbox + weight: 120 + region: content + settings: + display_label: true + third_party_settings: { } +hidden: + uid: true diff --git a/modules/ldap_courses_sync/config/optional/core.entity_view_display.group.course.default.yml b/modules/ldap_courses_sync/config/optional/core.entity_view_display.group.course.default.yml new file mode 100644 index 0000000..06f2e5f --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/core.entity_view_display.group.course.default.yml @@ -0,0 +1,81 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.field.group.course.field_course_code + - field.field.group.course.field_course_coord + - field.field.group.course.field_course_coord_assoc + - field.field.group.course.field_course_mail + - field.field.group.course.field_course_phone + - field.field.group.course.field_course_department + - group.type.course + module: + - options +id: group.course.default +targetEntityType: group +bundle: course +mode: default +content: + field_course_code: + type: string + label: above + settings: + link_to_entity: false + third_party_settings: { } + weight: -3 + region: content + field_course_mail: + type: basic_string + label: above + settings: { } + third_party_settings: { } + weight: 0 + region: content + field_course_phone: + type: string + label: above + settings: + link_to_entity: false + third_party_settings: { } + weight: -1 + region: content + field_course_coord: + type: entity_reference_label + label: above + settings: + link: true + third_party_settings: { } + weight: 1 + region: content + field_course_coord_assoc: + type: entity_reference_label + label: above + settings: + link: true + third_party_settings: { } + weight: 2 + region: content + field_course_department: + type: entity_reference_label + label: above + settings: + link: true + third_party_settings: { } + weight: 25 + region: content + label: + type: string + label: hidden + settings: + link_to_entity: false + third_party_settings: { } + weight: -5 + region: content +hidden: + changed: true + created: true + entity_print_view_epub: true + entity_print_view_pdf: true + entity_print_view_word_docx: true + langcode: true + uid: true diff --git a/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_code.yml b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_code.yml new file mode 100644 index 0000000..e6ae29d --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_code.yml @@ -0,0 +1,18 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.group.field_course_code + - group.type.course +id: group.course.field_course_code +field_name: field_course_code +entity_type: group +bundle: course +label: 'Código' +description: 'Código do curso (chave de sincronização LDAP)' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_coord.yml b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_coord.yml new file mode 100644 index 0000000..ffc078e --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_coord.yml @@ -0,0 +1,30 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.group.field_course_coord + - group.type.course + module: + - user +id: group.course.field_course_coord +field_name: field_course_coord +entity_type: group +bundle: course +label: Coordenador +description: 'Usuário responsável pela coordenação do curso' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: + target_bundles: null + sort: + field: _none + direction: ASC + auto_create: false + filter: + type: _none + include_anonymous: false +field_type: entity_reference diff --git a/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_coord_assoc.yml b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_coord_assoc.yml new file mode 100644 index 0000000..ffece98 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_coord_assoc.yml @@ -0,0 +1,30 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.group.field_course_coord_assoc + - group.type.course + module: + - user +id: group.course.field_course_coord_assoc +field_name: field_course_coord_assoc +entity_type: group +bundle: course +label: 'Coordenador Associado' +description: 'Usuário responsável pela coordenação associada do curso' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: + target_bundles: null + sort: + field: _none + direction: ASC + auto_create: false + filter: + type: _none + include_anonymous: false +field_type: entity_reference diff --git a/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_department.yml b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_department.yml new file mode 100644 index 0000000..b048250 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_department.yml @@ -0,0 +1,28 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.group.field_course_department + - group.type.department + - group.type.course +id: group.course.field_course_department +field_name: field_course_department +entity_type: group +bundle: course +label: Departamento +description: 'Departamento ao qual este curso está vinculado' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + handler: 'default:group' + handler_settings: + target_bundles: + department: department + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_mail.yml b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_mail.yml new file mode 100644 index 0000000..ac8f640 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_mail.yml @@ -0,0 +1,18 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.group.field_course_mail + - group.type.course +id: group.course.field_course_mail +field_name: field_course_mail +entity_type: group +bundle: course +label: 'E-mail' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: email diff --git a/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_phone.yml b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_phone.yml new file mode 100644 index 0000000..c4496b1 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.group.course.field_course_phone.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.group.field_course_phone + - group.type.course + module: + - telephone +id: group.course.field_course_phone +field_name: field_course_phone +entity_type: group +bundle: course +label: 'Telefone' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: telephone diff --git a/modules/ldap_courses_sync/config/optional/field.field.user.user.field_user_courses.yml b/modules/ldap_courses_sync/config/optional/field.field.user.user.field_user_courses.yml new file mode 100644 index 0000000..b5b4a42 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.field.user.user.field_user_courses.yml @@ -0,0 +1,28 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_courses + module: + - user +id: user.user.field_user_courses +field_name: field_user_courses +entity_type: user +bundle: user +label: 'Cursos' +description: 'Cursos do usuário sincronizados do LDAP' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + handler: 'default:group' + handler_settings: + target_bundles: + course: course + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_code.yml b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_code.yml new file mode 100644 index 0000000..0b6b011 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_code.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + module: + - group +id: group.field_course_code +field_name: field_course_code +entity_type: group +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_coord.yml b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_coord.yml new file mode 100644 index 0000000..8fa267b --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_coord.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + module: + - group + - user +id: group.field_course_coord +field_name: field_course_coord +entity_type: group +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_coord_assoc.yml b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_coord_assoc.yml new file mode 100644 index 0000000..c47c145 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_coord_assoc.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + module: + - group + - user +id: group.field_course_coord_assoc +field_name: field_course_coord_assoc +entity_type: group +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_department.yml b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_department.yml new file mode 100644 index 0000000..f4a08c4 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_department.yml @@ -0,0 +1,18 @@ +langcode: pt-br +status: true +dependencies: + module: + - group +id: group.field_course_department +field_name: field_course_department +entity_type: group +type: entity_reference +settings: + target_type: group +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_mail.yml b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_mail.yml new file mode 100644 index 0000000..528f037 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_mail.yml @@ -0,0 +1,17 @@ +langcode: pt-br +status: true +dependencies: + module: + - group +id: group.field_course_mail +field_name: field_course_mail +entity_type: group +type: email +settings: { } +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_phone.yml b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_phone.yml new file mode 100644 index 0000000..3e919a6 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.group.field_course_phone.yml @@ -0,0 +1,18 @@ +langcode: pt-br +status: true +dependencies: + module: + - group + - telephone +id: group.field_course_phone +field_name: field_course_phone +entity_type: group +type: telephone +settings: { } +module: telephone +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/field.storage.user.field_user_courses.yml b/modules/ldap_courses_sync/config/optional/field.storage.user.field_user_courses.yml new file mode 100644 index 0000000..e6698e1 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/field.storage.user.field_user_courses.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + module: + - group + - user +id: user.field_user_courses +field_name: field_user_courses +entity_type: user +type: entity_reference +settings: + target_type: group +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ldap_courses_sync/config/optional/group.role.course-admin.yml b/modules/ldap_courses_sync/config/optional/group.role.course-admin.yml new file mode 100644 index 0000000..9aef4f6 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.role.course-admin.yml @@ -0,0 +1,37 @@ +langcode: pt-br +status: true +dependencies: + config: + - group.type.course +id: course-admin +label: Admin +weight: 100 +admin: true +scope: individual +global_role: null +group_type: course +permissions: + - administer members + - delete group + - edit group + - leave group + - view group + - view unpublished group + - access group_media overview + - access group_node overview + - create group_node:article entity + - create group_node:page entity + - delete any group_node:article entity + - delete any group_node:page entity + - delete own group_node:article entity + - delete own group_node:page entity + - update any group_node:article entity + - update any group_node:page entity + - update own group_node:article entity + - update own group_node:page entity + - view group_node:article entity + - view group_node:page entity + - view own unpublished group_node:article entity + - view own unpublished group_node:page entity + - view unpublished group_node:article entity + - view unpublished group_node:page entity diff --git a/modules/ldap_courses_sync/config/optional/group.role.course-admin_in.yml b/modules/ldap_courses_sync/config/optional/group.role.course-admin_in.yml new file mode 100644 index 0000000..ca4061c --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.role.course-admin_in.yml @@ -0,0 +1,38 @@ +langcode: pt-br +status: true +dependencies: + config: + - group.type.course + - user.role.administrator +id: course-admin_in +label: Administrador +weight: 102 +admin: true +scope: insider +global_role: administrator +group_type: course +permissions: + - administer members + - delete group + - edit group + - leave group + - view group + - view unpublished group + - access group_media overview + - access group_node overview + - create group_node:article entity + - create group_node:page entity + - delete any group_node:article entity + - delete any group_node:page entity + - delete own group_node:article entity + - delete own group_node:page entity + - update any group_node:article entity + - update any group_node:page entity + - update own group_node:article entity + - update own group_node:page entity + - view group_node:article entity + - view group_node:page entity + - view own unpublished group_node:article entity + - view own unpublished group_node:page entity + - view unpublished group_node:article entity + - view unpublished group_node:page entity diff --git a/modules/ldap_courses_sync/config/optional/group.role.course-admin_out.yml b/modules/ldap_courses_sync/config/optional/group.role.course-admin_out.yml new file mode 100644 index 0000000..a05037e --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.role.course-admin_out.yml @@ -0,0 +1,38 @@ +langcode: pt-br +status: true +dependencies: + config: + - group.type.course + - user.role.administrator +id: course-admin_out +label: Administrador +weight: 101 +admin: true +scope: outsider +global_role: administrator +group_type: course +permissions: + - administer members + - delete group + - edit group + - join group + - view group + - view unpublished group + - access group_media overview + - access group_node overview + - create group_node:article entity + - create group_node:page entity + - delete any group_node:article entity + - delete any group_node:page entity + - delete own group_node:article entity + - delete own group_node:page entity + - update any group_node:article entity + - update any group_node:page entity + - update own group_node:article entity + - update own group_node:page entity + - view group_node:article entity + - view group_node:page entity + - view own unpublished group_node:article entity + - view own unpublished group_node:page entity + - view unpublished group_node:article entity + - view unpublished group_node:page entity diff --git a/modules/ldap_courses_sync/config/optional/group.role.course-anonymous.yml b/modules/ldap_courses_sync/config/optional/group.role.course-anonymous.yml new file mode 100644 index 0000000..993201f --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.role.course-anonymous.yml @@ -0,0 +1,17 @@ +langcode: pt-br +status: true +dependencies: + config: + - group.type.course + - user.role.anonymous +id: course-anonymous +label: 'Anônimo' +weight: -102 +admin: false +scope: outsider +global_role: anonymous +group_type: course +permissions: + - view group + - view group_node:article entity + - view group_node:page entity diff --git a/modules/ldap_courses_sync/config/optional/group.role.course-member.yml b/modules/ldap_courses_sync/config/optional/group.role.course-member.yml new file mode 100644 index 0000000..3c02f38 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.role.course-member.yml @@ -0,0 +1,27 @@ +langcode: pt-br +status: true +dependencies: + config: + - group.type.course + - user.role.authenticated +id: course-member +label: Member +weight: -100 +admin: false +scope: insider +global_role: authenticated +group_type: course +permissions: + - view group + - access group_media overview + - access group_node overview + - create group_node:article entity + - create group_node:page entity + - delete own group_node:article entity + - delete own group_node:page entity + - update own group_node:article entity + - update own group_node:page entity + - view group_node:article entity + - view group_node:page entity + - view own unpublished group_node:article entity + - view own unpublished group_node:page entity diff --git a/modules/ldap_courses_sync/config/optional/group.role.course-outsider.yml b/modules/ldap_courses_sync/config/optional/group.role.course-outsider.yml new file mode 100644 index 0000000..3d1a659 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.role.course-outsider.yml @@ -0,0 +1,17 @@ +langcode: pt-br +status: true +dependencies: + config: + - group.type.course + - user.role.authenticated +id: course-outsider +label: Outsider +weight: -101 +admin: false +scope: outsider +global_role: authenticated +group_type: course +permissions: + - view group + - view group_node:article entity + - view group_node:page entity diff --git a/modules/ldap_courses_sync/config/optional/group.type.course.yml b/modules/ldap_courses_sync/config/optional/group.type.course.yml new file mode 100644 index 0000000..7ae73e9 --- /dev/null +++ b/modules/ldap_courses_sync/config/optional/group.type.course.yml @@ -0,0 +1,10 @@ +langcode: pt-br +status: true +dependencies: { } +id: course +label: 'Curso' +description: '' +new_revision: true +creator_membership: true +creator_wizard: true +creator_roles: { } diff --git a/modules/ldap_courses_sync/css/role-mapping.css b/modules/ldap_courses_sync/css/role-mapping.css new file mode 100644 index 0000000..f5f17b1 --- /dev/null +++ b/modules/ldap_courses_sync/css/role-mapping.css @@ -0,0 +1,73 @@ +/** + * Role mapping table styles + */ + +/* Limita a largura da tabela e permite scroll horizontal se necessário */ +.role-mappings-table { + max-width: 100%; + table-layout: fixed; +} + +/* Define larguras fixas para cada coluna */ +.role-mappings-table th:nth-child(1), +.role-mappings-table td:nth-child(1) { + width: 20%; + min-width: 150px; +} + +.role-mappings-table th:nth-child(2), +.role-mappings-table td:nth-child(2) { + width: 15%; + min-width: 130px; +} + +.role-mappings-table th:nth-child(3), +.role-mappings-table td:nth-child(3) { + width: 25%; + min-width: 180px; +} + +.role-mappings-table th:nth-child(4), +.role-mappings-table td:nth-child(4) { + width: 30%; + min-width: 200px; +} + +.role-mappings-table th:nth-child(5), +.role-mappings-table td:nth-child(5) { + width: 10%; + min-width: 80px; + text-align: center; +} + +/* Estilo dos selects para limitar largura e truncar texto */ +.role-mapping-field-select { + max-width: 100%; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Estilo para options dentro dos selects - mostra texto completo no dropdown */ +.role-mapping-field-select option { + white-space: normal; + overflow: visible; + text-overflow: clip; +} + +/* Estilo do textarea */ +.role-mapping-values-textarea { + max-width: 100%; + width: 100%; + min-height: 50px; +} + +/* Responsividade: em telas menores, permite scroll horizontal */ +@media (max-width: 1200px) { + .role-mappings-table { + display: block; + overflow-x: auto; + white-space: nowrap; + } +} diff --git a/modules/ldap_courses_sync/ldap_courses_sync.info.yml b/modules/ldap_courses_sync/ldap_courses_sync.info.yml new file mode 100644 index 0000000..8c35057 --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.info.yml @@ -0,0 +1,13 @@ +name: LDAP Courses Sync +type: module +description: 'Sincroniza cursos do servidor LDAP com grupos via cron' +version: '1.0.0' +core_version_requirement: ^11 +package: Custom +dependencies: + - ldap_groups_sync:ldap_groups_sync + - drupal:options + - drupal:telephone + - group:group + - ldap:ldap_servers + - site_tools diff --git a/modules/ldap_courses_sync/ldap_courses_sync.install b/modules/ldap_courses_sync/ldap_courses_sync.install new file mode 100644 index 0000000..694a66e --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.install @@ -0,0 +1,23 @@ +addWarning(t('Please ensure your group type has the required fields: field_course_code, field_course_phone, field_course_mail, and field_course_department (for department reference).')); +} + +/** + * Implements hook_uninstall(). + */ +function ldap_courses_sync_uninstall() { + // Remove configurações + \Drupal::configFactory()->getEditable('ldap_courses_sync.settings')->delete(); + + \Drupal::messenger()->addStatus(t('LDAP Courses Sync module uninstalled.')); +} diff --git a/modules/ldap_courses_sync/ldap_courses_sync.libraries.yml b/modules/ldap_courses_sync/ldap_courses_sync.libraries.yml new file mode 100644 index 0000000..da24478 --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.libraries.yml @@ -0,0 +1,5 @@ +role_mapping_styles: + version: 1.x + css: + theme: + css/role-mapping.css: {} diff --git a/modules/ldap_courses_sync/ldap_courses_sync.links.menu.yml b/modules/ldap_courses_sync/ldap_courses_sync.links.menu.yml new file mode 100644 index 0000000..456d3c8 --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.links.menu.yml @@ -0,0 +1 @@ +# Menu entry removed: the parent module ldap_groups_sync provides the unified menu entry. diff --git a/modules/ldap_courses_sync/ldap_courses_sync.links.task.yml b/modules/ldap_courses_sync/ldap_courses_sync.links.task.yml new file mode 100644 index 0000000..384e279 --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.links.task.yml @@ -0,0 +1,5 @@ +ldap_courses_sync.tab.config: + title: 'Courses Sync' + route_name: ldap_courses_sync.config + base_route: ldap_groups_sync.config + weight: 30 diff --git a/modules/ldap_courses_sync/ldap_courses_sync.module b/modules/ldap_courses_sync/ldap_courses_sync.module new file mode 100644 index 0000000..8a4aa1f --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.module @@ -0,0 +1,236 @@ +' . t('This module synchronizes courses from an LDAP server to groups through cron.') . '

'; + } +} + +/** + * Implements hook_cron(). + */ +function ldap_courses_sync_cron() { + // Check if module is properly configured + $config = \Drupal::config('ldap_courses_sync.settings'); + $ldap_server_id = $config->get('ldap_server_id'); + $ldap_query_id = $config->get('ldap_query_id'); + + // Validate LDAP server + try { + $entity_type_manager = \Drupal::entityTypeManager(); + $server_storage = $entity_type_manager->getStorage('ldap_server'); + $server = $server_storage->load($ldap_server_id); + + if (!$server || !$server->get('status')) { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: LDAP server "@server_id" not found or inactive. Configure at /admin/config/local-modules/ldap-courses-sync', [ + '@server_id' => $ldap_server_id, + ]); + return; + } + } + catch (\Exception $e) { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking LDAP server: @message', [ + '@message' => $e->getMessage(), + ]); + return; + } + + // Validate LDAP query + try { + if ($entity_type_manager->hasDefinition('ldap_query_entity')) { + $query_storage = $entity_type_manager->getStorage('ldap_query_entity'); + $query = $query_storage->load($ldap_query_id); + + if (!$query || !$query->get('status')) { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: LDAP query "@query_id" not found or inactive. Configure at /admin/config/local-modules/ldap-courses-sync', [ + '@query_id' => $ldap_query_id, + ]); + return; + } + } + else { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: ldap_query module not available.'); + return; + } + } + catch (\Exception $e) { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking LDAP query: @message', [ + '@message' => $e->getMessage(), + ]); + return; + } + + // Validate group type + try { + $group_type_id = $config->get('group_type_id'); + $group_type_storage = $entity_type_manager->getStorage('group_type'); + $group_type = $group_type_storage->load($group_type_id); + + if (!$group_type) { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: group type "@type_id" not found. Configure at /admin/config/local-modules/ldap-courses-sync', [ + '@type_id' => $group_type_id, + ]); + return; + } + } + catch (\Exception $e) { + \Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking group type: @message', [ + '@message' => $e->getMessage(), + ]); + return; + } + + // Get LDAP synchronization service + $ldap_sync = \Drupal::service('ldap_courses_sync.sync'); + + // Execute courses synchronization + try { + $ldap_sync->syncCourses(); + \Drupal::logger('ldap_courses_sync')->info('Courses synchronization executed successfully.'); + } + catch (\Exception $e) { + \Drupal::logger('ldap_courses_sync')->error('Error in courses synchronization: @message', [ + '@message' => $e->getMessage(), + ]); + } +} + +/** + * Implements hook_entity_access(). + * + * Evaluates access rules configured in the module settings for view, update + * and delete operations on existing entities. + */ +function ldap_courses_sync_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + // Skip group-related entities to avoid conflicts with the group module. + $skip_types = ['group', 'group_content', 'group_relationship', 'group_role', 'group_type']; + if (in_array($entity->getEntityTypeId(), $skip_types, TRUE)) { + return AccessResult::neutral(); + } + + return \Drupal::service('ldap_courses_sync.access_rules') + ->checkAccess($entity, $operation, $account); +} + +/** + * Implements hook_entity_create_access(). + * + * Evaluates access rules configured in the module settings for create + * operations on new entities. + */ +function ldap_courses_sync_entity_create_access(AccountInterface $account, array $context, $entity_bundle) { + $entity_type_id = $context['entity_type_id'] ?? ''; + + // Skip group-related entities. + $skip_types = ['group', 'group_content', 'group_relationship', 'group_role', 'group_type']; + if (in_array($entity_type_id, $skip_types, TRUE)) { + return AccessResult::neutral(); + } + + return \Drupal::service('ldap_courses_sync.access_rules') + ->checkCreateAccess($account, $entity_type_id, $entity_bundle ?? ''); +} + +/** + * Implements hook_entity_field_access(). + * + * Denies edit access to fields managed by the LDAP sync in forms and APIs + * (REST, JSON:API). Programmatic saves are not affected. + * + * Protected user fields: + * - field_user_courses: multi-value reference to the courses the user belongs + * to, populated by our sync. Protecting this field prevents the validation + * error "This entity cannot be referenced" that occurs when the widget + * tries to validate the referenced group against the current user's access. + */ +function ldap_courses_sync_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) { + $protected_user_fields = [ + 'field_user_courses', + ]; + + if ( + $field_definition->getTargetEntityTypeId() === 'user' && + in_array($field_definition->getName(), $protected_user_fields, TRUE) && + $operation === 'edit' + ) { + // Sempre nega edição via UI/API, inclusive para o uid 1. + // Saves programáticos (ldap_user, drush) não passam por este hook. + return AccessResult::forbidden('This field is managed exclusively by LDAP synchronization.'); + } + return AccessResult::neutral(); +} + +/** + * Implements hook_user_presave(). + * + * Protects fields managed by the LDAP sync from unauthorized programmatic + * changes on existing users. Allows: + * - Saves during authorized LDAP sync (LdapCoursesSync::$syncing TRUE) + * - New user creation (initial provisioning via ldap_user) + * - Saves by users with the 'edit ldap managed user course fields' permission + */ +function ldap_courses_sync_user_presave(\Drupal\user\UserInterface $user) { + // Permite durante qualquer sync LDAP autorizado. + if (LdapCoursesSync::isSyncing()) { + return; + } + + // Permite na criação inicial do usuário (primeiro login LDAP). + if ($user->isNew()) { + return; + } + + // Permite se o usuário atual tem a permissão de bypass. + if (\Drupal::currentUser()->hasPermission('edit ldap managed user course fields')) { + return; + } + + $original = $user->original; + if ($original === NULL) { + return; + } + + // Fields managed by LDAP that cannot be changed externally. + $protected_fields = [ + 'field_user_courses', + ]; + + foreach ($protected_fields as $field_name) { + if (!$user->hasField($field_name)) { + continue; + } + + $original_value = $original->get($field_name)->getValue(); + $new_value = $user->get($field_name)->getValue(); + + if ($original_value !== $new_value) { + $user->set($field_name, $original_value); + \Drupal::logger('ldap_courses_sync')->warning( + 'Unauthorized attempt to change @field on user @username was blocked. Use LDAP synchronization to update this field.', + [ + '@field' => $field_name, + '@username' => $user->getAccountName(), + ] + ); + } + } +} diff --git a/modules/ldap_courses_sync/ldap_courses_sync.permissions.yml b/modules/ldap_courses_sync/ldap_courses_sync.permissions.yml new file mode 100644 index 0000000..fbba76b --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.permissions.yml @@ -0,0 +1,9 @@ +administer ldap courses sync: + title: 'Administer LDAP Courses Sync' + description: 'Configure the LDAP courses synchronization.' + restrict access: true + +edit ldap managed user course fields: + title: 'Edit LDAP-managed course fields on users' + description: 'Allows manually editing fields such as field_user_courses that are normally managed by LDAP synchronization. Use only in emergencies.' + restrict access: true diff --git a/modules/ldap_courses_sync/ldap_courses_sync.routing.yml b/modules/ldap_courses_sync/ldap_courses_sync.routing.yml new file mode 100644 index 0000000..0592755 --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.routing.yml @@ -0,0 +1,7 @@ +ldap_courses_sync.config: + path: '/admin/config/local-modules/ldap-courses-sync' + defaults: + _form: '\Drupal\ldap_courses_sync\Form\LdapCoursesSyncConfigForm' + _title: 'LDAP Courses Sync' + requirements: + _permission: 'administer ldap courses sync' diff --git a/modules/ldap_courses_sync/ldap_courses_sync.services.yml b/modules/ldap_courses_sync/ldap_courses_sync.services.yml new file mode 100644 index 0000000..a9640ce --- /dev/null +++ b/modules/ldap_courses_sync/ldap_courses_sync.services.yml @@ -0,0 +1,8 @@ +services: + ldap_courses_sync.sync: + class: Drupal\ldap_courses_sync\LdapCoursesSync + arguments: ['@entity_type.manager', '@config.factory', '@logger.factory', '@ldap.bridge'] + + ldap_courses_sync.access_rules: + class: Drupal\ldap_groups_sync\GroupAccessRulesService + arguments: ['@config.factory', '@entity_type.manager', 'ldap_courses_sync.settings'] diff --git a/modules/ldap_courses_sync/src/Form/LdapCoursesSyncConfigForm.php b/modules/ldap_courses_sync/src/Form/LdapCoursesSyncConfigForm.php new file mode 100644 index 0000000..95d3842 --- /dev/null +++ b/modules/ldap_courses_sync/src/Form/LdapCoursesSyncConfigForm.php @@ -0,0 +1,1310 @@ +entityTypeManager = $entity_type_manager; + $this->ldapCoursesSync = $ldap_courses_sync; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('ldap_courses_sync.sync'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['ldap_courses_sync.settings']; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ldap_courses_sync_config_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('ldap_courses_sync.settings'); + + // Group Type + $form['group_type'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Group Type'), + '#collapsible' => FALSE, + '#prefix' => '
', + '#suffix' => '
', + ]; + + $group_types = $this->getGroupTypes(); + + $form['group_type']['group_type_id'] = [ + '#type' => 'select', + '#title' => $this->t('Target Group Type'), + '#description' => $this->t('Select the group type where courses will be synchronized.
Required fields: field_course_code, field_course_phone, field_course_mail'), + '#options' => $group_types, + '#empty_option' => $this->t('- Select a group type -'), + '#default_value' => $config->get('group_type_id') ?: 'course', + '#required' => TRUE, + '#ajax' => [ + 'callback' => '::updateGroupTypeActions', + 'wrapper' => 'group-type-wrapper', + 'effect' => 'fade', + 'disable-refocus' => TRUE, + ], + '#limit_validation_errors' => [ + ['group_type_id'], + ], + ]; + + $form['group_type']['group_type_actions'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['form-actions', 'group-type-actions']], + '#prefix' => '
', + '#suffix' => '
', + ]; + + $course_exists = isset($group_types['course']); + + if (empty($group_types) || !$course_exists) { + $form['group_type']['setup_notice'] = [ + '#type' => 'markup', + '#markup' => '
' . + $this->t('The default "course" group type is not installed.') . + '
', + '#weight' => -10, + ]; + + $form['group_type']['group_type_actions']['install_defaults'] = [ + '#type' => 'submit', + '#value' => $this->t('Install Default Configuration'), + '#submit' => ['::installDefaultConfiguration'], + '#button_type' => 'primary', + '#limit_validation_errors' => [], + '#attributes' => ['class' => ['button', 'button--primary']], + ]; + + $form['group_type']['install_help'] = [ + '#type' => 'markup', + '#markup' => '
' . + $this->t('The button above will create:Or you can create a custom group type manually.', [ + '@url' => '/admin/group/types/add', + ]) . '
', + '#weight' => 100, + ]; + } + + $form['group_type']['group_type_actions']['create_group_type'] = [ + '#type' => 'markup', + '#markup' => '' . + $this->t('Create New Group Type') . ' ', + ]; + + $selected_group_type = $form_state->getValue('group_type_id') ?: $config->get('group_type_id') ?: 'course'; + if (!empty($selected_group_type)) { + $form['group_type']['group_type_actions']['edit_group_type'] = [ + '#type' => 'markup', + '#markup' => '' . + $this->t('Manage Group Type') . ' ', + ]; + + $form['group_type']['group_type_actions']['edit_group_type_fields'] = [ + '#type' => 'markup', + '#markup' => '' . + $this->t('Manage Fields') . '', + ]; + } + + // LDAP Server + $form['ldap_server'] = [ + '#type' => 'fieldset', + '#title' => $this->t('LDAP Server'), + '#collapsible' => FALSE, + ]; + + $ldap_servers = $this->getLdapServers(); + + $form['ldap_server']['ldap_server_id'] = [ + '#type' => 'select', + '#title' => $this->t('LDAP Server'), + '#description' => $this->t('Select the LDAP server configured for courses synchronization.'), + '#options' => $ldap_servers, + '#empty_option' => $this->t('- Select a server -'), + '#default_value' => $config->get('ldap_server_id'), + '#required' => TRUE, + '#ajax' => [ + 'callback' => '::updateLdapQueries', + 'wrapper' => 'ldap-queries-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [ + ['group_type_id'], + ['ldap_server_id'], + ], + ]; + + if (empty($ldap_servers)) { + $form['ldap_server']['no_servers'] = [ + '#type' => 'markup', + '#markup' => '
' . + $this->t('No LDAP server found. Configure an LDAP server first.', [ + '@url' => '/admin/config/people/ldap/servers', + ]) . '
', + ]; + } + + // LDAP Query + $form['ldap_query'] = [ + '#type' => 'fieldset', + '#title' => $this->t('LDAP Query'), + '#collapsible' => FALSE, + '#prefix' => '
', + '#suffix' => '
', + ]; + + $selected_server = $form_state->getValue('ldap_server_id') ?: $config->get('ldap_server_id'); + $ldap_queries = $this->getLdapQueriesForServer($selected_server); + + $form['ldap_query']['ldap_query_id'] = [ + '#type' => 'select', + '#title' => $this->t('LDAP Query'), + '#description' => $this->t('Select the LDAP query that will be used to search for courses.'), + '#options' => $ldap_queries, + '#empty_option' => $this->t('- Select a query -'), + '#default_value' => $config->get('ldap_query_id'), + '#required' => TRUE, + '#ajax' => [ + 'callback' => '::updateLdapQueries', + 'wrapper' => 'ldap-queries-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [ + ['group_type_id'], + ['ldap_server_id'], + ['ldap_query_id'], + ], + ]; + + if (empty($ldap_queries) && !empty($selected_server)) { + $form['ldap_query']['no_queries'] = [ + '#type' => 'markup', + '#markup' => '
' . + $this->t('No LDAP query found for the selected server. Create a new LDAP query.', [ + '@url' => '/admin/config/people/ldap/query/add', + ]) . '
', + ]; + } + + if (!empty($selected_server)) { + $form['ldap_query']['query_actions'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['form-actions', 'query-actions']], + '#prefix' => '
', + '#suffix' => '
', + ]; + + $form['ldap_query']['query_actions']['create_query'] = [ + '#type' => 'markup', + '#markup' => '' . + $this->t('Create New LDAP Query') . ' ', + ]; + + $selected_query = $form_state->getValue('ldap_query_id') ?: $config->get('ldap_query_id'); + if (!empty($selected_query)) { + $form['ldap_query']['query_actions']['edit_query'] = [ + '#type' => 'markup', + '#markup' => '' . + $this->t('Edit Selected Query') . '', + ]; + } + } + + // Dynamic attribute mapping + $form['attribute_mapping'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Attribute Mapping'), + '#description' => $this->t('Configure how LDAP attributes are mapped to target entity fields.
Note: For "User Reference" mappings, the search attribute will be automatically obtained from the "Account name attribute" field configured in the selected LDAP server.'), + '#collapsible' => FALSE, + '#prefix' => '
', + '#suffix' => '
', + ]; + + $selected_group_type = $form_state->getValue('group_type_id') ?: $config->get('group_type_id') ?: 'course'; + $entity_fields = $this->getGroupTypeFields($selected_group_type); + + $selected_server = $form_state->getValue('ldap_server_id') ?: $config->get('ldap_server_id'); + $selected_query = $form_state->getValue('ldap_query_id') ?: $config->get('ldap_query_id'); + $ldap_attributes = $this->getQueryAttributes($selected_server, $selected_query); + + $mappings_reset = $form_state->get('mappings_reset'); + $form_state_mappings = $form_state->getValue('mappings'); + + if ($mappings_reset && !empty($form_state_mappings)) { + $existing_mappings = $form_state_mappings; + } + elseif (!empty($form_state_mappings)) { + $existing_mappings = []; + $valid_fields = array_keys($entity_fields); + + foreach ($form_state_mappings as $mapping) { + if (empty($mapping['field']) || in_array($mapping['field'], $valid_fields)) { + $existing_mappings[] = $mapping; + } + } + + if (empty($existing_mappings)) { + $existing_mappings = $this->getDefaultMappings(); + } + } + else { + $config_mappings = $config->get('attribute_mappings'); + + if (!empty($config_mappings)) { + $valid_mappings = []; + $valid_fields = array_keys($entity_fields); + + foreach ($config_mappings as $mapping) { + if (empty($mapping['field']) || in_array($mapping['field'], $valid_fields)) { + $valid_mappings[] = $mapping; + } + } + + $existing_mappings = !empty($valid_mappings) ? $valid_mappings : $this->getDefaultMappings(); + } + else { + $existing_mappings = $this->getDefaultMappings(); + } + } + + $form['attribute_mapping']['mappings'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Entity Field'), + $this->t('LDAP Attribute'), + $this->t('Mapping Type'), + $this->t('Remove'), + ], + '#empty' => $this->t('No mappings configured.'), + ]; + + $mapping_count = $form_state->get('mapping_count') ?: count($existing_mappings); + $form_state->set('mapping_count', $mapping_count); + + for ($i = 0; $i < $mapping_count; $i++) { + $mapping = isset($existing_mappings[$i]) ? $existing_mappings[$i] : [ + 'field' => '', + 'attribute' => '', + 'mapping_type' => 'simple', + ]; + + $field_default = isset($mapping['field']) ? $mapping['field'] : ''; + if (!empty($field_default) && !isset($entity_fields[$field_default])) { + $field_default = ''; + } + + $attribute_default = isset($mapping['attribute']) ? $mapping['attribute'] : ''; + if (!empty($attribute_default) && !isset($ldap_attributes[$attribute_default])) { + $attribute_default = ''; + } + + $field_element = [ + '#type' => 'select', + '#options' => $entity_fields, + '#empty_option' => $this->t('- Select a field -'), + '#default_value' => $field_default, + '#required' => FALSE, + '#attributes' => [ + 'style' => 'max-width: 200px; width: auto;', + ], + ]; + + if ($mappings_reset && !empty($field_default)) { + $field_element['#value'] = $field_default; + } + + $form['attribute_mapping']['mappings'][$i]['field'] = $field_element; + + $form['attribute_mapping']['mappings'][$i]['attribute'] = [ + '#type' => 'select', + '#options' => $ldap_attributes, + '#empty_option' => $this->t('- Select an attribute -'), + '#default_value' => $attribute_default, + '#required' => FALSE, + '#attributes' => [ + 'style' => 'max-width: 180px; width: auto;', + ], + ]; + + $form['attribute_mapping']['mappings'][$i]['mapping_type'] = [ + '#type' => 'select', + '#options' => [ + 'simple' => $this->t('Simple (text)'), + 'user_reference' => $this->t('User Reference (DN → User)'), + 'department_reference' => $this->t('Department Reference (code → Department)'), + ], + '#default_value' => $mapping['mapping_type'] ?? 'simple', + '#required' => FALSE, + '#attributes' => [ + 'style' => 'max-width: 150px; width: auto;', + ], + ]; + + $form['attribute_mapping']['mappings'][$i]['remove'] = [ + '#type' => 'checkbox', + '#default_value' => FALSE, + ]; + } + + if ($mappings_reset) { + $form_state->set('mappings_reset', FALSE); + } + + $form['attribute_mapping']['actions'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['form-actions']], + ]; + + $form['attribute_mapping']['actions']['add_mapping'] = [ + '#type' => 'submit', + '#value' => $this->t('Add Mapping'), + '#submit' => ['::addMapping'], + '#ajax' => [ + 'callback' => '::updateAttributeMappings', + 'wrapper' => 'attribute-mapping-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [], + ]; + + $form['attribute_mapping']['actions']['remove_selected'] = [ + '#type' => 'submit', + '#value' => $this->t('Remove Selected'), + '#submit' => ['::removeSelectedMappings'], + '#ajax' => [ + 'callback' => '::updateAttributeMappings', + 'wrapper' => 'attribute-mapping-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [], + ]; + + // Member synchronization from LDAP attribute + $form['member_sync'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Member Synchronization'), + ]; + + $form['member_sync']['member_sync_enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable member synchronization from LDAP attribute'), + '#description' => $this->t('When enabled, group memberships are managed based on the LDAP member attribute of each group entry. Members are added or removed to match the LDAP list.'), + '#default_value' => $config->get('member_sync_enabled') ?? TRUE, + ]; + + $form['member_sync']['member_attribute'] = [ + '#type' => 'textfield', + '#title' => $this->t('Member LDAP attribute'), + '#description' => $this->t('Name of the LDAP attribute on each group entry that lists its members (as DNs or UIDs). Common values: member, uniqueMember, memberUid. The attribute must be included in the LDAP query.'), + '#default_value' => $config->get('member_attribute') ?: 'member', + '#size' => 40, + ]; + + // Role Mapping + $form['role_mapping'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Group Role Mapping'), + '#description' => $this->t('Configure how user attributes map to group roles. Each role can have its own mapping criteria.'), + '#collapsible' => FALSE, + '#prefix' => '
', + '#suffix' => '
', + ]; + + $form['role_mapping']['role_mapping_enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable role mapping'), + '#description' => $this->t('When enabled, users will be assigned group roles based on the mappings below.'), + '#default_value' => $config->get('role_mapping_enabled') ?: FALSE, + '#ajax' => [ + 'callback' => '::updateRoleMappingFields', + 'wrapper' => 'role-mapping-fields-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [ + ['group_type_id'], + ['ldap_server_id'], + ['ldap_query_id'], + ['role_mapping_enabled'], + ], + ]; + + $form['role_mapping']['role_mapping_fields'] = [ + '#type' => 'container', + '#prefix' => '
', + '#suffix' => '
', + ]; + + $role_mapping_enabled_value = $form_state->getValue('role_mapping_enabled'); + if ($role_mapping_enabled_value !== NULL) { + $role_mapping_enabled = (bool) $role_mapping_enabled_value; + } + else { + $role_mapping_enabled = $config->get('role_mapping_enabled') ?: FALSE; + } + + if ($role_mapping_enabled) { + $group_roles = $this->getGroupRoles($selected_group_type); + $role_ldap_attributes = $ldap_attributes ?? []; + $user_fields = $this->getUserFields(); + + $role_form_state_mappings = $form_state->getValue('role_mappings'); + if (!empty($role_form_state_mappings)) { + $existing_role_mappings = $role_form_state_mappings; + } + else { + $config_role_mappings = $config->get('role_mappings'); + $existing_role_mappings = $config_role_mappings ?: []; + } + + if (empty($existing_role_mappings)) { + $existing_role_mappings = []; + } + + $form['role_mapping']['role_mapping_fields']['role_mappings'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Group Role'), + $this->t('Source'), + $this->t('User Field / LDAP Attribute'), + $this->t('Group Field / Values'), + $this->t('Remove'), + ], + '#empty' => $this->t('No role mappings configured. Users will be assigned the default role.'), + '#attributes' => ['class' => ['role-mappings-table']], + '#attached' => [ + 'library' => ['ldap_courses_sync/role_mapping_styles'], + ], + ]; + + $role_mapping_count = $form_state->get('role_mapping_count'); + if ($role_mapping_count === NULL) { + $role_mapping_count = count($existing_role_mappings); + } + $form_state->set('role_mapping_count', $role_mapping_count); + + $group_fields = $this->getGroupTypeFields($selected_group_type); + + for ($i = 0; $i < $role_mapping_count; $i++) { + $mapping = isset($existing_role_mappings[$i]) ? $existing_role_mappings[$i] : [ + 'group_role' => '', + 'source' => 'user_field', + 'source_field' => '', + 'group_field' => '', + 'values' => '', + ]; + + $current_source = $form_state->getValue(['role_mappings', $i, 'source']) ?? $mapping['source'] ?? 'user_field'; + + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['group_role'] = [ + '#type' => 'select', + '#options' => $group_roles, + '#empty_option' => $this->t('- Select a role -'), + '#default_value' => $mapping['group_role'], + '#required' => FALSE, + ]; + + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['source'] = [ + '#type' => 'select', + '#options' => [ + 'user_field' => $this->t('User Field'), + 'ldap_attribute' => $this->t('LDAP Attribute'), + 'group_field_match' => $this->t('Group Field Match'), + ], + '#default_value' => $current_source, + '#required' => FALSE, + '#ajax' => [ + 'callback' => '::updateRoleMappingFields', + 'wrapper' => 'role-mapping-fields-wrapper', + 'effect' => 'fade', + ], + ]; + + if ($current_source === 'group_field_match') { + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['source_field'] = [ + '#type' => 'select', + '#options' => $user_fields, + '#empty_option' => $this->t('- Select user field -'), + '#default_value' => $mapping['source_field'], + '#required' => FALSE, + '#attributes' => ['class' => ['role-mapping-field-select']], + ]; + } + else { + if ($current_source === 'ldap_attribute') { + $source_options = $role_ldap_attributes; + } + else { + $source_options = $user_fields; + } + + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['source_field'] = [ + '#type' => 'select', + '#options' => $source_options, + '#empty_option' => $this->t('- Select attribute/field -'), + '#default_value' => $mapping['source_field'], + '#required' => FALSE, + '#attributes' => ['class' => ['role-mapping-field-select']], + ]; + } + + if ($current_source === 'group_field_match') { + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['values'] = [ + '#type' => 'select', + '#options' => $group_fields, + '#empty_option' => $this->t('- Select group field -'), + '#default_value' => $mapping['group_field'] ?? '', + '#required' => FALSE, + '#attributes' => ['class' => ['role-mapping-field-select']], + ]; + } + else { + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['values'] = [ + '#type' => 'textarea', + '#default_value' => is_array($mapping['values']) ? implode(', ', $mapping['values']) : $mapping['values'], + '#placeholder' => $this->t('e.g., Director, Manager'), + '#rows' => 2, + '#resizable' => 'vertical', + '#attributes' => ['class' => ['role-mapping-values-textarea']], + ]; + } + + $form['role_mapping']['role_mapping_fields']['role_mappings'][$i]['remove'] = [ + '#type' => 'checkbox', + '#default_value' => FALSE, + ]; + } + + $form['role_mapping']['role_mapping_fields']['actions'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['form-actions']], + ]; + + $form['role_mapping']['role_mapping_fields']['actions']['add_role_mapping'] = [ + '#type' => 'submit', + '#value' => $this->t('Add Role Mapping'), + '#submit' => ['::addRoleMapping'], + '#ajax' => [ + 'callback' => '::updateRoleMappingFields', + 'wrapper' => 'role-mapping-fields-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [], + ]; + + $form['role_mapping']['role_mapping_fields']['actions']['remove_selected_roles'] = [ + '#type' => 'submit', + '#value' => $this->t('Remove Selected'), + '#submit' => ['::removeSelectedRoleMappings'], + '#ajax' => [ + 'callback' => '::updateRoleMappingFields', + 'wrapper' => 'role-mapping-fields-wrapper', + 'effect' => 'fade', + ], + '#limit_validation_errors' => [], + ]; + } + + // Actions + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save Configuration'), + '#button_type' => 'primary', + ]; + + $form['actions']['test_connection'] = [ + '#type' => 'submit', + '#value' => $this->t('Test Connection'), + '#submit' => ['::testConnection'], + '#limit_validation_errors' => [['ldap_server_id']], + ]; + + $form['actions']['sync_courses'] = [ + '#type' => 'submit', + '#value' => $this->t('Synchronize Courses'), + '#submit' => ['::syncCourses'], + '#button_type' => 'danger', + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * Gets list of available LDAP servers. + */ + protected function getLdapServers() { + $servers = []; + + try { + $server_storage = $this->entityTypeManager->getStorage('ldap_server'); + $server_entities = $server_storage->loadByProperties(['status' => TRUE]); + + foreach ($server_entities as $server) { + $servers[$server->id()] = $server->label() . ' (' . $server->get('address') . ')'; + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading LDAP servers: @message', ['@message' => $e->getMessage()])); + } + + return $servers; + } + + /** + * Gets attributes from the selected LDAP query. + */ + protected function getQueryAttributes($server_id, $query_id) { + $attributes = []; + + if (empty($query_id) || !$this->entityTypeManager->hasDefinition('ldap_query_entity')) { + return [ + 'cn' => 'cn', + 'description' => 'description', + 'mail' => 'mail', + 'telephoneNumber' => 'telephoneNumber', + ]; + } + + try { + $query_storage = $this->entityTypeManager->getStorage('ldap_query_entity'); + $query_entity = $query_storage->load($query_id); + + if ($query_entity) { + $processed_attributes = $query_entity->getProcessedAttributes(); + + if (!empty($processed_attributes)) { + foreach ($processed_attributes as $attribute) { + $attributes[$attribute] = $attribute; + } + } + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading query attributes: @message', ['@message' => $e->getMessage()])); + } + + if (empty($attributes)) { + $attributes = [ + 'cn' => 'cn', + 'description' => 'description', + 'mail' => 'mail', + 'telephoneNumber' => 'telephoneNumber', + ]; + } + + return $attributes; + } + + /** + * Returns default mappings for courses. + */ + protected function getDefaultMappings() { + return [ + ['field' => 'label', 'attribute' => 'description', 'mapping_type' => 'simple'], + ['field' => 'field_course_code', 'attribute' => 'cn', 'mapping_type' => 'simple'], + ['field' => 'field_course_mail', 'attribute' => 'mail', 'mapping_type' => 'simple'], + ['field' => 'field_course_phone', 'attribute' => 'telephoneNumber', 'mapping_type' => 'simple'], + ]; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $mapping_values = $form_state->getValue('mappings'); + if (!empty($mapping_values)) { + foreach ($mapping_values as $index => $mapping_data) { + if (!empty($mapping_data['remove'])) { + continue; + } + + $has_field = !empty($mapping_data['field']); + $has_attribute = !empty($mapping_data['attribute']); + + if ($has_field && !$has_attribute) { + $form_state->setErrorByName("mappings][$index][attribute", + $this->t('LDAP attribute is required when field is selected.')); + } + elseif (!$has_field && $has_attribute) { + $form_state->setErrorByName("mappings][$index][field", + $this->t('Vocabulary field is required when attribute is selected.')); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $config = $this->config('ldap_courses_sync.settings'); + + $config->set('ldap_server_id', $form_state->getValue('ldap_server_id')) + ->set('ldap_query_id', $form_state->getValue('ldap_query_id')) + ->set('group_type_id', $form_state->getValue('group_type_id')) + ->set('member_sync_enabled', $form_state->getValue('member_sync_enabled')) + ->set('member_attribute', $form_state->getValue('member_attribute')); + + $mappings = []; + $mapping_values = $form_state->getValue('mappings'); + + if (!empty($mapping_values)) { + foreach ($mapping_values as $mapping_data) { + if (!empty($mapping_data['remove'])) { + continue; + } + + if (empty($mapping_data['field']) && empty($mapping_data['attribute'])) { + continue; + } + + $mappings[] = [ + 'field' => $mapping_data['field'], + 'attribute' => $mapping_data['attribute'], + 'mapping_type' => $mapping_data['mapping_type'] ?? 'simple', + ]; + } + } + + $config->set('attribute_mappings', $mappings); + + $role_mappings = []; + $role_mapping_values = $form_state->getValue('role_mappings'); + + if (!empty($role_mapping_values)) { + foreach ($role_mapping_values as $role_mapping_data) { + if (!empty($role_mapping_data['remove'])) { + continue; + } + + if (empty($role_mapping_data['group_role']) && empty($role_mapping_data['source_field'])) { + continue; + } + + $source = $role_mapping_data['source']; + + if ($source === 'group_field_match') { + $role_mapping = [ + 'group_role' => $role_mapping_data['group_role'], + 'source' => $source, + 'source_field' => $role_mapping_data['source_field'], + 'group_field' => $role_mapping_data['values'], + 'values' => [], + ]; + } + else { + $values = $role_mapping_data['values']; + if (is_string($values)) { + $values = array_map('trim', explode(',', $values)); + $values = array_filter($values); + } + + $role_mapping = [ + 'group_role' => $role_mapping_data['group_role'], + 'source' => $source, + 'source_field' => $role_mapping_data['source_field'], + 'group_field' => '', + 'values' => $values, + ]; + } + + $role_mappings[] = $role_mapping; + } + } + + $config->set('role_mapping_enabled', $form_state->getValue('role_mapping_enabled')) + ->set('role_mappings', $role_mappings) + ->save(); + + $this->messenger()->addStatus($this->t('Configuration saved successfully.')); + } + + /** + * Submit handler to test LDAP connection. + */ + public function testConnection(array &$form, FormStateInterface $form_state) { + $server_id = $form_state->getValue('ldap_server_id'); + + if (empty($server_id)) { + $this->messenger->addError($this->t('Please select an LDAP server to test.')); + return; + } + + try { + $server_storage = $this->entityTypeManager->getStorage('ldap_server'); + $server = $server_storage->load($server_id); + + if ($server && $server->get('status')) { + $this->messenger->addStatus($this->t('LDAP server "@server" is configured and active.', [ + '@server' => $server->label(), + ])); + } + else { + $this->messenger->addError($this->t('LDAP server not found or inactive.')); + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error testing connection: @message', ['@message' => $e->getMessage()])); + } + } + + /** + * Submit handler para sincronizar cursos. + */ + public function syncCourses(array &$form, FormStateInterface $form_state) { + if ($form_state->getErrors()) { + $this->messenger->addError($this->t('Please fix the errors in the form before running the synchronization.')); + return; + } + + try { + $this->ldapCoursesSync->syncCourses(); + $this->messenger->addStatus($this->t('Synchronization completed successfully. Check the logs for details.')); + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error during synchronization: @message', ['@message' => $e->getMessage()])); + } + } + + /** + * Callback AJAX para atualizar lista de queries quando servidor muda. + */ + public function updateLdapQueries(array &$form, FormStateInterface $form_state) { + return $form['ldap_query']; + } + + /** + * Submit handler para adicionar um novo mapeamento. + */ + public function addMapping(array &$form, FormStateInterface $form_state) { + $current_mappings = $form_state->getValue('mappings'); + + if (empty($current_mappings)) { + $config = $this->config('ldap_courses_sync.settings'); + $current_mappings = $config->get('attribute_mappings') ?: $this->getDefaultMappings(); + } + + $processed_mappings = []; + foreach ($current_mappings as $mapping) { + $processed_mappings[] = [ + 'field' => $mapping['field'] ?? '', + 'attribute' => $mapping['attribute'] ?? '', + 'mapping_type' => $mapping['mapping_type'] ?? 'simple', + 'remove' => FALSE, + ]; + } + + $processed_mappings[] = [ + 'field' => '', + 'attribute' => '', + 'mapping_type' => 'simple', + 'remove' => FALSE, + ]; + + $form_state->setValue('mappings', $processed_mappings); + $form_state->set('mapping_count', count($processed_mappings)); + $form_state->setRebuild(); + } + + /** + * Submit handler para remover mapeamentos selecionados. + */ + public function removeSelectedMappings(array &$form, FormStateInterface $form_state) { + $mapping_values = $form_state->getValue('mappings'); + $new_mappings = []; + + if (!empty($mapping_values)) { + foreach ($mapping_values as $mapping_data) { + if (empty($mapping_data['remove'])) { + $new_mappings[] = $mapping_data; + } + } + } + + $form_state->setValue('mappings', $new_mappings); + $form_state->set('mapping_count', count($new_mappings)); + $form_state->setRebuild(); + } + + /** + * Callback AJAX para atualizar mapeamentos de atributos. + */ + public function updateAttributeMappings(array &$form, FormStateInterface $form_state) { + return $form['attribute_mapping']; + } + + /** + * Submit handler to add a new role mapping. + */ + public function addRoleMapping(array &$form, FormStateInterface $form_state) { + $current_mappings = $form_state->getValue('role_mappings'); + + if (empty($current_mappings)) { + $config = $this->config('ldap_courses_sync.settings'); + $current_mappings = $config->get('role_mappings') ?: []; + } + + $processed_mappings = []; + foreach ($current_mappings as $mapping) { + $processed_mappings[] = [ + 'group_role' => $mapping['group_role'] ?? '', + 'source' => $mapping['source'] ?? 'ldap_attribute', + 'source_field' => $mapping['source_field'] ?? '', + 'values' => $mapping['values'] ?? '', + 'remove' => FALSE, + ]; + } + + $processed_mappings[] = [ + 'group_role' => '', + 'source' => 'ldap_attribute', + 'source_field' => '', + 'values' => '', + 'remove' => FALSE, + ]; + + $form_state->setValue('role_mappings', $processed_mappings); + $form_state->set('role_mapping_count', count($processed_mappings)); + $form_state->setRebuild(); + } + + /** + * Submit handler to remove selected role mappings. + */ + public function removeSelectedRoleMappings(array &$form, FormStateInterface $form_state) { + $mapping_values = $form_state->getValue('role_mappings'); + $new_mappings = []; + + if (!empty($mapping_values)) { + foreach ($mapping_values as $mapping_data) { + if (empty($mapping_data['remove'])) { + $new_mappings[] = $mapping_data; + } + } + } + + $form_state->setValue('role_mappings', $new_mappings); + $form_state->set('role_mapping_count', count($new_mappings)); + $form_state->setRebuild(); + } + + /** + * AJAX callback to update role mapping fields. + */ + public function updateRoleMappingFields(array &$form, FormStateInterface $form_state) { + return $form['role_mapping']['role_mapping_fields']; + } + + /** + * Gets available roles for the selected group type. + */ + protected function getGroupRoles($group_type_id) { + $roles = []; + + if (empty($group_type_id)) { + return $roles; + } + + try { + $group_type_storage = $this->entityTypeManager->getStorage('group_type'); + $group_type = $group_type_storage->load($group_type_id); + + if ($group_type) { + $group_role_storage = $this->entityTypeManager->getStorage('group_role'); + $group_roles = $group_role_storage->loadByProperties([ + 'group_type' => $group_type_id, + ]); + + foreach ($group_roles as $role) { + $role_id = $role->id(); + + if (strpos($role_id, '-anonymous') !== FALSE || strpos($role_id, '-outsider') !== FALSE) { + continue; + } + + $scope_label = ''; + + if (strpos($role_id, '_in') !== FALSE) { + $scope_label = ' (' . $this->t('Internal') . ')'; + } + elseif (strpos($role_id, '_out') !== FALSE) { + $scope_label = ' (' . $this->t('Outsider') . ')'; + } + + $roles[$role_id] = $role->label() . $scope_label; + } + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading group roles: @message', ['@message' => $e->getMessage()])); + } + + return $roles; + } + + /** + * Gets available user fields for mapping. + */ + protected function getUserFields() { + $fields = []; + + try { + $field_manager = \Drupal::service('entity_field.manager'); + $field_definitions = $field_manager->getFieldDefinitions('user', 'user'); + + foreach ($field_definitions as $field_name => $field_definition) { + if (in_array($field_name, ['uid', 'uuid', 'langcode', 'name', 'pass', 'mail', 'timezone', 'status', 'created', 'changed', 'access', 'login', 'init', 'roles', 'default_langcode'])) { + continue; + } + + $fields[$field_name] = $field_definition->getLabel() . ' (' . $field_name . ')'; + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading user fields: @message', ['@message' => $e->getMessage()])); + } + + return $fields; + } + + /** + * Obtém lista de queries LDAP para um servidor específico. + */ + protected function getLdapQueriesForServer($server_id) { + $queries = []; + + if (empty($server_id)) { + return $queries; + } + + if (!$this->entityTypeManager->hasDefinition('ldap_query_entity')) { + $this->messenger->addWarning($this->t('The ldap_query module is not available. LDAP queries cannot be loaded.')); + return $queries; + } + + try { + $query_storage = $this->entityTypeManager->getStorage('ldap_query_entity'); + $query_entities = $query_storage->loadByProperties([ + 'status' => TRUE, + 'server_id' => $server_id, + ]); + + foreach ($query_entities as $query) { + $queries[$query->id()] = $query->label() . ' (' . $query->get('base_dn') . ')'; + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading LDAP queries: @message', ['@message' => $e->getMessage()])); + } + + return $queries; + } + + /** + * Gets list of available group types. + */ + protected function getGroupTypes() { + $group_types = []; + + try { + $group_type_storage = $this->entityTypeManager->getStorage('group_type'); + $group_type_entities = $group_type_storage->loadMultiple(); + + foreach ($group_type_entities as $group_type) { + $group_types[$group_type->id()] = $group_type->label() . ' (' . $group_type->id() . ')'; + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading group types: @message', ['@message' => $e->getMessage()])); + } + + return $group_types; + } + + /** + * Gets fields from the selected group type. + */ + protected function getGroupTypeFields($group_type_id) { + $fields = [ + 'label' => $this->t('Label (group title)'), + ]; + + if (empty($group_type_id)) { + return $fields; + } + + try { + $group_type_storage = $this->entityTypeManager->getStorage('group_type'); + $group_type = $group_type_storage->load($group_type_id); + + if (!$group_type) { + $this->messenger->addWarning($this->t('Group type "@type" not found. Please create it first or select a different group type.', [ + '@type' => $group_type_id, + ])); + return $fields; + } + + $field_manager = \Drupal::service('entity_field.manager'); + $field_definitions = $field_manager->getFieldDefinitions('group', $group_type_id); + + foreach ($field_definitions as $field_name => $field_definition) { + if (in_array($field_name, ['id', 'uuid', 'type', 'langcode', 'label', 'uid', 'created', 'changed', 'default_langcode'])) { + continue; + } + + $fields[$field_name] = $field_definition->getLabel() . ' (' . $field_name . ')'; + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error loading group type fields: @message', ['@message' => $e->getMessage()])); + } + + return $fields; + } + + /** + * Callback AJAX to update group type actions when group type changes. + */ + public function updateGroupTypeActions(array &$form, FormStateInterface $form_state) { + return $form['group_type']; + } + + /** + * Submit handler to install default configuration. + */ + public function installDefaultConfiguration(array &$form, FormStateInterface $form_state) { + $module_path = \Drupal::service('extension.list.module')->getPath('ldap_courses_sync'); + $config_path = $module_path . '/config/optional'; + + if (!is_dir($config_path)) { + $this->messenger->addError($this->t('Configuration directory not found: @path', ['@path' => $config_path])); + return; + } + + try { + $config_factory = \Drupal::configFactory(); + $yaml_parser = \Drupal::service('serialization.yaml'); + $files_imported = 0; + + $import_order = [ + 'field.storage.*.yml', + 'group.type.*.yml', + 'group.role.*.yml', + 'field.field.*.yml', + 'core.entity_*.yml', + ]; + + foreach ($import_order as $pattern) { + $files = glob($config_path . '/' . $pattern); + foreach ($files as $file) { + $config_name = basename($file, '.yml'); + + $existing_config = $config_factory->get($config_name); + if (!$existing_config->isNew()) { + $this->messenger->addWarning($this->t('Configuration @name already exists, skipping.', ['@name' => $config_name])); + continue; + } + + $yaml_content = file_get_contents($file); + $data = $yaml_parser->decode($yaml_content); + + $config_factory->getEditable($config_name) + ->setData($data) + ->save(); + + $files_imported++; + } + } + + if ($files_imported > 0) { + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + \Drupal::service('plugin.manager.field.widget')->clearCachedDefinitions(); + \Drupal::service('plugin.manager.field.formatter')->clearCachedDefinitions(); + \Drupal::service('config.factory')->clearStaticCache(); + \Drupal::service('router.builder')->rebuild(); + + $this->messenger->addStatus($this->t('Successfully imported @count configuration files.', ['@count' => $files_imported])); + } + else { + $this->messenger->addWarning($this->t('No new configuration files were imported. All configurations already exist.')); + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('Error importing configuration: @message', ['@message' => $e->getMessage()])); + } + } + +} diff --git a/modules/ldap_courses_sync/src/LdapCoursesSync.php b/modules/ldap_courses_sync/src/LdapCoursesSync.php new file mode 100644 index 0000000..3ace5af --- /dev/null +++ b/modules/ldap_courses_sync/src/LdapCoursesSync.php @@ -0,0 +1,1142 @@ +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'; + } + +} diff --git a/modules/ldap_courses_sync/src/Plugin/EntityReferenceSelection/CourseSelection.php b/modules/ldap_courses_sync/src/Plugin/EntityReferenceSelection/CourseSelection.php new file mode 100644 index 0000000..999ac35 --- /dev/null +++ b/modules/ldap_courses_sync/src/Plugin/EntityReferenceSelection/CourseSelection.php @@ -0,0 +1,31 @@ +condition('type', 'course'); + return $query; + } + +} diff --git a/modules/ldap_courses_sync/translations/ldap_courses_sync.pt-br.po b/modules/ldap_courses_sync/translations/ldap_courses_sync.pt-br.po new file mode 100644 index 0000000..a29cbb5 --- /dev/null +++ b/modules/ldap_courses_sync/translations/ldap_courses_sync.pt-br.po @@ -0,0 +1,54 @@ +# Portuguese (Brazil) translation for LDAP Courses Sync module +# Copyright (c) 2026 +# This file is distributed under the same license as the LDAP Courses Sync module. +# +msgid "" +msgstr "" +"Project-Id-Version: LDAP Courses Sync 1.0.0\n" +"POT-Creation-Date: 2026-03-02 00:00+0000\n" +"PO-Revision-Date: 2026-03-02 00:00+0000\n" +"Language-Team: Portuguese (Brazil)\n" +"Language: pt-br\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: ldap_courses_sync.routing.yml +msgid "LDAP Courses Sync" +msgstr "Sincronização de Cursos LDAP" + +#: ldap_courses_sync.links.task.yml +msgid "Courses Sync" +msgstr "Sincronização de Cursos" + +#: ldap_courses_sync.module +msgid "This module synchronizes courses from an LDAP server to groups through cron." +msgstr "Este módulo sincroniza cursos de um servidor LDAP para grupos através do cron." + +#: ldap_courses_sync.install +msgid "Please ensure your group type has the required fields: field_course_code, field_course_phone, field_course_mail, and field_course_department (for department reference)." +msgstr "Certifique-se de que o tipo de grupo possui os campos obrigatórios: field_course_code, field_course_phone, field_course_mail e field_course_department (para referência de departamento)." + +msgid "LDAP Courses Sync module uninstalled." +msgstr "Módulo LDAP Courses Sync desinstalado." + +#: src/Form/LdapCoursesSyncConfigForm.php + +msgid "Select the group type where courses will be synchronized.
Required fields: field_course_code, field_course_phone, field_course_mail" +msgstr "Selecione o tipo de grupo onde os cursos serão sincronizados.
Campos obrigatórios: field_course_code, field_course_phone, field_course_mail" + +msgid "The default \"course\" group type is not installed." +msgstr "O tipo de grupo padrão \"course\" não está instalado." + +msgid "The button above will create:Or you can create a custom group type manually." +msgstr "O botão acima irá criar:Ou você pode criar um tipo de grupo personalizado manualmente." + +msgid "Select the LDAP server configured for courses synchronization." +msgstr "Selecione o servidor LDAP configurado para sincronização de cursos." + +msgid "Select the LDAP query that will be used to search for courses." +msgstr "Selecione a query LDAP que será usada para buscar cursos." + +msgid "Synchronize Courses" +msgstr "Sincronizar Cursos" diff --git a/src/Form/GlobalAccessRuleForm.php b/src/Form/GlobalAccessRuleForm.php index 52806a7..8088bd4 100644 --- a/src/Form/GlobalAccessRuleForm.php +++ b/src/Form/GlobalAccessRuleForm.php @@ -102,6 +102,9 @@ class GlobalAccessRuleForm extends AccessRuleFormBase { if ($this->moduleHandler->moduleExists('ldap_research_groups_sync')) { $options['research_groups'] = $this->t('Research Groups'); } + if ($this->moduleHandler->moduleExists('ldap_courses_sync')) { + $options['courses'] = $this->t('Courses'); + } $form['group_type'] = [ '#type' => 'select', @@ -131,6 +134,7 @@ class GlobalAccessRuleForm extends AccessRuleFormBase { protected function getConfigName(): string { return match($this->groupType) { 'research_groups' => 'ldap_research_groups_sync.settings', + 'courses' => 'ldap_courses_sync.settings', default => 'ldap_departments_sync.settings', }; } @@ -141,6 +145,7 @@ class GlobalAccessRuleForm extends AccessRuleFormBase { protected function getDefaultGroupTypeId(): string { return match($this->groupType) { 'research_groups' => 'research_group', + 'courses' => 'course', default => 'departments', }; } diff --git a/src/Form/UnifiedAccessRulesForm.php b/src/Form/UnifiedAccessRulesForm.php index 6327575..0a29153 100644 --- a/src/Form/UnifiedAccessRulesForm.php +++ b/src/Form/UnifiedAccessRulesForm.php @@ -33,6 +33,11 @@ class UnifiedAccessRulesForm extends FormBase { 'module' => 'ldap_research_groups_sync', 'label' => 'Research Groups', ], + 'courses' => [ + 'config' => 'ldap_courses_sync.settings', + 'module' => 'ldap_courses_sync', + 'label' => 'Courses', + ], ]; /**