Inicializa módulo base ldap_groups_sync

Cria super-módulo com infraestrutura compartilhada de regras de acesso
para os módulos de sincronização LDAP de grupos.

- GroupAccessRulesService: serviço parametrizável por config name
- AccessRulesFormBase: listagem/remoção de regras (classe abstrata)
- AccessRuleFormBase: formulário modal de criação/edição (classe abstrata)
- Sub-módulos ldap_departments_sync e ldap_research_groups_sync refatorados
  para estender as classes base com subclasses mínimas
- Traduções pt-br centralizadas em ldap_groups_sync.pt-br.po

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 09:55:54 -03:00
commit 346b897e25
104 changed files with 11315 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
# LDAP Research Groups Sync
Módulo Drupal 11 que sincroniza grupos de pesquisa do servidor LDAP corporativo com entidades do tipo `research_group` (módulo Group).
## Funcionalidades
- Criação e atualização automática de grupos de pesquisa via cron a partir de uma LDAP query configurável
- Sincronização de membros dos grupos a partir do atributo `member` (ou outro configurável) de cada entrada LDAP
- Regras de acesso configuráveis por grupo
- Proteção dos campos LDAP do usuário contra edição não autorizada
## Dependências
- `drupal:options`
- `drupal:telephone`
- `group:group`
- `ldap:ldap_servers`
- `site_tools`
## Campos criados
### No grupo (`research_group`)
| Campo | Tipo | Descrição |
|---|---|---|
| `field_rg_code` | String | Código do grupo (chave de sincronização) |
| `field_rg_phone` | Telephone | Telefone |
| `field_rg_mail` | Email | E-mail |
| `field_rg_coord` | Entity reference (user) | Coordenador do grupo |
| `field_rg_coord_assoc` | Entity reference (user) | Coordenador associado do grupo |
| `field_rg_department` | Entity reference (group) | Departamento ao qual o grupo está vinculado |
### No usuário
| Campo | Tipo | Descrição |
|---|---|---|
| `field_user_research_groups` | Entity reference (group, múltiplos) | Grupos de pesquisa do usuário (populado pelo sync) |
## Instalação
```bash
drush en ldap_research_groups_sync
drush cr
```
## Configuração
Acesse `/admin/config/local-modules/ldap-research-groups-sync` e configure:
1. **Group Type** — selecione `research_group`
2. **LDAP Server** — servidor LDAP a ser usado
3. **LDAP Query** — query entity que retorna as entradas dos grupos de pesquisa
4. **Attribute Mappings** — mapeamento entre atributos LDAP e campos do grupo
5. **Member Synchronization** — habilite e defina o atributo LDAP de membros (padrão: `member`)
> **Atenção:** o atributo de membros (ex.: `member`) deve estar incluído nos atributos retornados pela LDAP query, ou a query não deve ter filtro de atributos.
## Sincronização manual
```bash
drush php-eval "\Drupal::service('ldap_research_groups_sync.sync')->syncResearchGroups();"
```
A sincronização também é executada automaticamente pelo cron.
## Permissões
- `administer ldap research groups sync` — acesso à página de configuração
- `edit ldap managed user rg fields` — permite editar o campo `field_user_research_groups` manualmente (normalmente reservado ao sync)

View File

@@ -0,0 +1,102 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.research_group.field_rg_code
- field.field.group.research_group.field_rg_coord
- field.field.group.research_group.field_rg_coord_assoc
- field.field.group.research_group.field_rg_mail
- field.field.group.research_group.field_rg_phone
- field.field.group.research_group.field_rg_department
- group.type.research_group
module:
- path
id: group.research_group.default
targetEntityType: group
bundle: research_group
mode: default
content:
field_rg_code:
type: string_textfield
weight: 122
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_rg_mail:
type: email_default
weight: 125
region: content
settings:
placeholder: ''
size: 60
third_party_settings: { }
field_rg_phone:
type: string_textfield
weight: 124
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_rg_coord:
type: entity_reference_autocomplete
weight: 126
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_rg_coord_assoc:
type: entity_reference_autocomplete
weight: 127
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_rg_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

View File

@@ -0,0 +1,81 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.research_group.field_rg_code
- field.field.group.research_group.field_rg_coord
- field.field.group.research_group.field_rg_coord_assoc
- field.field.group.research_group.field_rg_mail
- field.field.group.research_group.field_rg_phone
- field.field.group.research_group.field_rg_department
- group.type.research_group
module:
- options
id: group.research_group.default
targetEntityType: group
bundle: research_group
mode: default
content:
field_rg_code:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -3
region: content
field_rg_mail:
type: basic_string
label: above
settings: { }
third_party_settings: { }
weight: 0
region: content
field_rg_phone:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -1
region: content
field_rg_coord:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 1
region: content
field_rg_coord_assoc:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 2
region: content
field_rg_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

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_code
- group.type.research_group
id: group.research_group.field_rg_code
field_name: field_rg_code
entity_type: group
bundle: research_group
label: 'Código'
description: 'Código do grupo de pesquisa (chave de sincronização LDAP)'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_coord
- group.type.research_group
module:
- user
id: group.research_group.field_rg_coord
field_name: field_rg_coord
entity_type: group
bundle: research_group
label: Coordenador
description: 'Usuário responsável pela coordenação do grupo de pesquisa'
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

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_coord_assoc
- group.type.research_group
module:
- user
id: group.research_group.field_rg_coord_assoc
field_name: field_rg_coord_assoc
entity_type: group
bundle: research_group
label: 'Coordenador Associado'
description: 'Usuário responsável pela coordenação associada do grupo de pesquisa'
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

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_department
- group.type.department
- group.type.research_group
id: group.research_group.field_rg_department
field_name: field_rg_department
entity_type: group
bundle: research_group
label: Departamento
description: 'Departamento ao qual este grupo de pesquisa 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

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_mail
- group.type.research_group
id: group.research_group.field_rg_mail
field_name: field_rg_mail
entity_type: group
bundle: research_group
label: 'E-mail'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: email

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_phone
- group.type.research_group
module:
- telephone
id: group.research_group.field_rg_phone
field_name: field_rg_phone
entity_type: group
bundle: research_group
label: 'Telefone'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: telephone

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_research_groups
module:
- user
id: user.user.field_user_research_groups
field_name: field_user_research_groups
entity_type: user
bundle: user
label: 'Grupos de Pesquisa'
description: 'Grupos de pesquisa do usuário sincronizados do LDAP'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:group'
handler_settings:
target_bundles:
research_group: research_group
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_rg_code
field_name: field_rg_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

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_rg_coord
field_name: field_rg_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

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_rg_coord_assoc
field_name: field_rg_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

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_rg_department
field_name: field_rg_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

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_rg_mail
field_name: field_rg_mail
entity_type: group
type: email
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- telephone
id: group.field_rg_phone
field_name: field_rg_phone
entity_type: group
type: telephone
settings: { }
module: telephone
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: user.field_user_research_groups
field_name: field_user_research_groups
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

View File

@@ -0,0 +1,37 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
id: research_group-admin
label: Admin
weight: 100
admin: true
scope: individual
global_role: null
group_type: research_group
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

View File

@@ -0,0 +1,38 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.administrator
id: research_group-admin_in
label: Administrador
weight: 102
admin: true
scope: insider
global_role: administrator
group_type: research_group
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

View File

@@ -0,0 +1,38 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.administrator
id: research_group-admin_out
label: Administrador
weight: 101
admin: true
scope: outsider
global_role: administrator
group_type: research_group
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

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.anonymous
id: research_group-anonymous
label: 'Anônimo'
weight: -102
admin: false
scope: outsider
global_role: anonymous
group_type: research_group
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,27 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.authenticated
id: research_group-member
label: Member
weight: -100
admin: false
scope: insider
global_role: authenticated
group_type: research_group
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

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.authenticated
id: research_group-outsider
label: Outsider
weight: -101
admin: false
scope: outsider
global_role: authenticated
group_type: research_group
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,10 @@
langcode: pt-br
status: true
dependencies: { }
id: research_group
label: 'Grupo de Pesquisa'
description: ''
new_revision: true
creator_membership: true
creator_wizard: true
creator_roles: { }

View File

@@ -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;
}
}

View File

@@ -0,0 +1,13 @@
name: LDAP Research Groups Sync
type: module
description: 'Sincroniza grupos de pesquisa 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

View File

@@ -0,0 +1,94 @@
<?php
/**
* @file
* Arquivo de instalação para ldap_research_groups_sync.
*/
/**
* Implements hook_install().
*/
function ldap_research_groups_sync_install() {
\Drupal::messenger()->addWarning(t('Please ensure your group type has the required fields: field_rg_code, field_rg_phone, field_rg_mail, and field_rg_department (for department reference).'));
}
/**
* Adiciona os campos field_rg_coord e field_rg_coord_assoc ao bundle research_group.
*/
function ldap_research_groups_sync_update_10001() {
$configs = [
'field.storage.group.field_rg_coord',
'field.storage.group.field_rg_coord_assoc',
'field.field.group.research_group.field_rg_coord',
'field.field.group.research_group.field_rg_coord_assoc',
];
$module_path = \Drupal::service('extension.list.module')->getPath('ldap_research_groups_sync');
$config_path = $module_path . '/config/optional';
$config_factory = \Drupal::configFactory();
$yaml_parser = \Drupal::service('serialization.yaml');
$imported = 0;
foreach ($configs as $config_name) {
if (!$config_factory->get($config_name)->isNew()) {
continue;
}
$file = $config_path . '/' . $config_name . '.yml';
$data = $yaml_parser->decode(file_get_contents($file));
$config_factory->getEditable($config_name)->setData($data)->save();
$imported++;
}
if ($imported > 0) {
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
}
return t('Campos field_rg_coord e field_rg_coord_assoc importados (@count configs).', ['@count' => $imported]);
}
/**
* Adiciona o campo field_rg_code ao bundle research_group.
*/
function ldap_research_groups_sync_update_10002() {
$configs = [
'field.storage.group.field_rg_code',
'field.field.group.research_group.field_rg_code',
];
$module_path = \Drupal::service('extension.list.module')->getPath('ldap_research_groups_sync');
$config_path = $module_path . '/config/optional';
$config_factory = \Drupal::configFactory();
$yaml_parser = \Drupal::service('serialization.yaml');
$imported = 0;
foreach ($configs as $config_name) {
if (!$config_factory->get($config_name)->isNew()) {
continue;
}
$file = $config_path . '/' . $config_name . '.yml';
$data = $yaml_parser->decode(file_get_contents($file));
$config_factory->getEditable($config_name)->setData($data)->save();
$imported++;
}
if ($imported > 0) {
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
}
return t('Campo field_rg_code importado (@count configs).', ['@count' => $imported]);
}
/**
* Implements hook_uninstall().
*/
function ldap_research_groups_sync_uninstall() {
// Remove configurações
\Drupal::configFactory()->getEditable('ldap_research_groups_sync.settings')->delete();
\Drupal::messenger()->addStatus(t('LDAP Research Groups Sync module uninstalled.'));
}

View File

@@ -0,0 +1,5 @@
role_mapping_styles:
version: 1.x
css:
theme:
css/role-mapping.css: {}

View File

@@ -0,0 +1,6 @@
ldap_research_groups_sync.config:
title: 'LDAP Research Groups Sync'
description: 'Configurar sincronização de grupos de pesquisa do LDAP'
route_name: ldap_research_groups_sync.config
parent: site_tools.admin_config
weight: 6

View File

@@ -0,0 +1,11 @@
ldap_research_groups_sync.tab.config:
title: 'Configuration'
route_name: ldap_research_groups_sync.config
base_route: ldap_research_groups_sync.config
weight: 0
ldap_research_groups_sync.tab.access_rules:
title: 'Access Rules'
route_name: ldap_research_groups_sync.access_rules
base_route: ldap_research_groups_sync.config
weight: 10

View File

@@ -0,0 +1,238 @@
<?php
/**
* @file
* Module to synchronize research groups from LDAP to groups.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\ldap_research_groups_sync\LdapResearchGroupsSync;
/**
* Implements hook_help().
*/
function ldap_research_groups_sync_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.ldap_research_groups_sync':
return '<p>' . t('This module synchronizes research groups from an LDAP server to groups through cron.') . '</p>';
}
}
/**
* Implements hook_cron().
*/
function ldap_research_groups_sync_cron() {
// Check if module is properly configured
$config = \Drupal::config('ldap_research_groups_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_research_groups_sync')->warning('Synchronization cancelled: LDAP server "@server_id" not found or inactive. Configure at /admin/config/local-modules/ldap-research-groups-sync', [
'@server_id' => $ldap_server_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_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_research_groups_sync')->warning('Synchronization cancelled: LDAP query "@query_id" not found or inactive. Configure at /admin/config/local-modules/ldap-research-groups-sync', [
'@query_id' => $ldap_query_id,
]);
return;
}
}
else {
\Drupal::logger('ldap_research_groups_sync')->warning('Synchronization cancelled: ldap_query module not available.');
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_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_research_groups_sync')->warning('Synchronization cancelled: group type "@type_id" not found. Configure at /admin/config/local-modules/ldap-research-groups-sync', [
'@type_id' => $group_type_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_sync')->warning('Synchronization cancelled: error checking group type: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Get LDAP synchronization service
$ldap_sync = \Drupal::service('ldap_research_groups_sync.sync');
// Execute research groups synchronization
try {
$ldap_sync->syncResearchGroups();
\Drupal::logger('ldap_research_groups_sync')->info('Research groups synchronization executed successfully.');
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_sync')->error('Error in research groups 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_research_groups_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_research_groups_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_research_groups_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_research_groups_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_research_groups: multi-value reference to the research groups
* 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_research_groups_sync_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
$protected_user_fields = [
'field_user_research_groups',
];
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 (LdapResearchGroupsSync::$syncing TRUE)
* - New user creation (initial provisioning via ldap_user)
* - Saves by users with the 'edit ldap managed user rg fields' permission
*/
function ldap_research_groups_sync_user_presave(\Drupal\user\UserInterface $user) {
// Permite durante qualquer sync LDAP autorizado.
if (LdapResearchGroupsSync::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 rg fields')) {
return;
}
$original = $user->original;
if ($original === NULL) {
return;
}
// Fields managed by LDAP that cannot be changed externally.
$protected_fields = [
'field_user_research_groups',
];
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_research_groups_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(),
]
);
}
}
}

View File

@@ -0,0 +1,9 @@
administer ldap research groups sync:
title: 'Administer LDAP Research Groups Sync'
description: 'Configure the LDAP research groups synchronization.'
restrict access: true
edit ldap managed user rg fields:
title: 'Edit LDAP-managed research group fields on users'
description: 'Allows manually editing fields such as field_user_research_groups that are normally managed by LDAP synchronization. Use only in emergencies.'
restrict access: true

View File

@@ -0,0 +1,25 @@
ldap_research_groups_sync.config:
path: '/admin/config/local-modules/ldap-research-groups-sync'
defaults:
_form: '\Drupal\ldap_research_groups_sync\Form\LdapResearchGroupsSyncConfigForm'
_title: 'LDAP Research Groups Sync'
requirements:
_permission: 'administer ldap research groups sync'
ldap_research_groups_sync.access_rules:
path: '/admin/config/local-modules/ldap-research-groups-sync/access-rules'
defaults:
_form: '\Drupal\ldap_research_groups_sync\Form\AccessRulesForm'
_title: 'Access Rules'
requirements:
_permission: 'administer ldap research groups sync'
ldap_research_groups_sync.access_rule_form:
path: '/admin/config/local-modules/ldap-research-groups-sync/access-rule/{rule_index}'
defaults:
_form: '\Drupal\ldap_research_groups_sync\Form\AccessRuleForm'
_title: 'Access Rule'
rule_index: 'new'
requirements:
_permission: 'administer ldap research groups sync'
rule_index: 'new|\d+'

View File

@@ -0,0 +1,8 @@
services:
ldap_research_groups_sync.sync:
class: Drupal\ldap_research_groups_sync\LdapResearchGroupsSync
arguments: ['@entity_type.manager', '@config.factory', '@logger.factory', '@ldap.bridge']
ldap_research_groups_sync.access_rules:
class: Drupal\ldap_groups_sync\GroupAccessRulesService
arguments: ['@config.factory', '@entity_type.manager', 'ldap_research_groups_sync.settings']

View File

@@ -0,0 +1,85 @@
<?php
/**
* @file
* Installs field_rg_department on the research_group group bundle.
*
* Usage (from Drupal root):
* drush php-script modules/custom/ldap_research_groups_sync/scripts/install_field_rg_department.php
*/
$module_path = \Drupal::service('extension.list.module')->getPath('ldap_research_groups_sync');
$config_path = $module_path . '/config/optional';
$config_factory = \Drupal::configFactory();
$yaml_parser = \Drupal::service('serialization.yaml');
$configs_to_install = [
'field.storage.group.field_rg_department',
'field.field.group.research_group.field_rg_department',
'core.entity_form_display.group.research_group.default',
'core.entity_view_display.group.research_group.default',
];
$imported = 0;
$skipped = 0;
foreach ($configs_to_install as $config_name) {
$file = $config_path . '/' . $config_name . '.yml';
if (!file_exists($file)) {
echo "ERROR: File not found: $file\n";
continue;
}
$existing = $config_factory->get($config_name);
// For display configs, always update (they already exist and need the new field).
$is_display = str_starts_with($config_name, 'core.entity_');
if (!$existing->isNew() && !$is_display) {
echo "SKIP: $config_name (already exists)\n";
$skipped++;
continue;
}
$data = $yaml_parser->decode(file_get_contents($file));
$config_factory->getEditable($config_name)->setData($data)->save();
echo "OK: $config_name\n";
$imported++;
}
if ($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();
// Create the database table for the new field storage.
$field_manager = \Drupal::service('entity_field.manager');
$field_manager->clearCachedFieldDefinitions();
$update_manager = \Drupal::entityDefinitionUpdateManager();
$storage_defs = $field_manager->getFieldStorageDefinitions('group');
if (isset($storage_defs['field_rg_department'])) {
$update_manager->installFieldStorageDefinition(
'field_rg_department',
'group',
'ldap_research_groups_sync',
$storage_defs['field_rg_department']
);
echo "Schema update applied (group__field_rg_department table created).\n";
}
else {
echo "WARNING: field_rg_department storage definition not found — table not created.\n";
}
\Drupal::service('router.builder')->rebuild();
echo "\nDone. Imported: $imported, Skipped: $skipped\n";
echo "Run 'drush cr' to clear all caches.\n";
}
else {
echo "\nNothing imported. Skipped: $skipped\n";
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\ldap_research_groups_sync\Form;
use Drupal\ldap_groups_sync\Form\AccessRuleFormBase;
/**
* Modal form for creating or editing a single access rule (Research Groups Sync).
*/
class AccessRuleForm extends AccessRuleFormBase {
/**
* {@inheritdoc}
*/
protected function getConfigName(): string {
return 'ldap_research_groups_sync.settings';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ldap_research_groups_sync_access_rule_form';
}
/**
* {@inheritdoc}
*/
protected function getAccessRulesRoute(): string {
return 'ldap_research_groups_sync.access_rules';
}
/**
* {@inheritdoc}
*/
protected function getDefaultGroupTypeId(): string {
return 'research_group';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\ldap_research_groups_sync\Form;
use Drupal\ldap_groups_sync\Form\AccessRulesFormBase;
/**
* Lists and manages access rules for the LDAP Research Groups Sync module.
*/
class AccessRulesForm extends AccessRulesFormBase {
/**
* {@inheritdoc}
*/
protected function getConfigName(): string {
return 'ldap_research_groups_sync.settings';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ldap_research_groups_sync_access_rules_form';
}
/**
* {@inheritdoc}
*/
protected function getAccessRuleFormRoute(): string {
return 'ldap_research_groups_sync.access_rule_form';
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\ldap_research_groups_sync\Plugin\EntityReferenceSelection;
use Drupal\group\Plugin\EntityReferenceSelection\GroupSelection;
/**
* Plugin for research group entity reference selection.
*
* Restricts selection to the 'research_group' group type.
*
* @EntityReferenceSelection(
* id = "default:research_group",
* label = @Translation("Research Group selection"),
* entity_types = {"group"},
* group = "default",
* weight = 1
* )
*/
class ResearchGroupSelection extends GroupSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$query->condition('type', 'research_group');
return $query;
}
}

View File

@@ -0,0 +1,298 @@
# Portuguese (Brazil) translation for LDAP Research Groups Sync module
# Copyright (c) 2026
# This file is distributed under the same license as the LDAP Research Groups Sync module.
#
msgid ""
msgstr ""
"Project-Id-Version: LDAP Research Groups Sync 1.0.0\n"
"POT-Creation-Date: 2026-02-27 00:00+0000\n"
"PO-Revision-Date: 2026-02-27 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_research_groups_sync.routing.yml
msgid "LDAP Research Groups Sync"
msgstr "Sincronização de Grupos de Pesquisa LDAP"
#: ldap_research_groups_sync.routing.yml
msgid "Access Rules"
msgstr "Regras de Acesso"
#: ldap_research_groups_sync.routing.yml
msgid "Access Rule"
msgstr "Regra de Acesso"
#: ldap_research_groups_sync.module
msgid "This module synchronizes research groups from an LDAP server to groups through cron."
msgstr "Este módulo sincroniza grupos de pesquisa de um servidor LDAP para grupos através do cron."
#: ldap_research_groups_sync.install
msgid "Please ensure your group type has the required fields: field_rg_code, field_rg_phone, field_rg_mail, and field_rg_department (for department reference)."
msgstr "Certifique-se de que o tipo de grupo possui os campos obrigatórios: field_rg_code, field_rg_phone, field_rg_mail e field_rg_department (para referência de departamento)."
msgid "LDAP Research Groups Sync module uninstalled."
msgstr "Módulo LDAP Research Groups Sync desinstalado."
#: src/Form/LdapResearchGroupsSyncConfigForm.php
msgid "Group Type"
msgstr "Tipo de Grupo"
msgid "Target Group Type"
msgstr "Tipo de Grupo Alvo"
msgid "Select the group type where research groups will be synchronized. <br><strong>Required fields:</strong> field_rg_code, field_rg_phone, field_rg_mail"
msgstr "Selecione o tipo de grupo onde os grupos de pesquisa serão sincronizados. <br><strong>Campos obrigatórios:</strong> field_rg_code, field_rg_phone, field_rg_mail"
msgid "- Select a group type -"
msgstr "- Selecione um tipo de grupo -"
msgid "The default \"research_group\" group type is not installed."
msgstr "O tipo de grupo padrão \"research_group\" não está instalado."
msgid "Install Default Configuration"
msgstr "Instalar Configuração Padrão"
msgid "The button above will create:<ul>\n <li>Group type \"research_group\" with all required fields</li>\n <li>User fields for research group synchronization</li>\n <li>Default group roles and displays</li>\n </ul>Or you can <a href=\"@url\">create a custom group type</a> manually."
msgstr "O botão acima irá criar:<ul>\n <li>Tipo de grupo \"research_group\" com todos os campos obrigatórios</li>\n <li>Campos de usuário para sincronização de grupo de pesquisa</li>\n <li>Papéis e exibições padrão do grupo</li>\n </ul>Ou você pode <a href=\"@url\">criar um tipo de grupo personalizado</a> manualmente."
msgid "Create New Group Type"
msgstr "Criar Novo Tipo de Grupo"
msgid "Manage Group Type"
msgstr "Gerenciar Tipo de Grupo"
msgid "Manage Fields"
msgstr "Gerenciar Campos"
msgid "LDAP Server"
msgstr "Servidor LDAP"
msgid "Select the LDAP server configured for research groups synchronization."
msgstr "Selecione o servidor LDAP configurado para sincronização de grupos de pesquisa."
msgid "- Select a server -"
msgstr "- Selecione um servidor -"
msgid "No LDAP server found. <a href=\"@url\">Configure an LDAP server</a> first."
msgstr "Nenhum servidor LDAP encontrado. <a href=\"@url\">Configure um servidor LDAP</a> primeiro."
msgid "LDAP Query"
msgstr "Consulta LDAP"
msgid "Select the LDAP query that will be used to search for research groups."
msgstr "Selecione a consulta LDAP que será usada para buscar grupos de pesquisa."
msgid "- Select a query -"
msgstr "- Selecione uma query -"
msgid "No LDAP query found for the selected server. <a href=\"@url\">Create a new LDAP query</a>."
msgstr "Nenhuma query LDAP encontrada para o servidor selecionado. <a href=\"@url\">Crie uma nova query LDAP</a>."
msgid "Create New LDAP Query"
msgstr "Criar Nova Query LDAP"
msgid "Edit Selected Query"
msgstr "Editar Query Selecionada"
msgid "Attribute Mapping"
msgstr "Mapeamento de Atributos"
msgid "Configure how LDAP attributes are mapped to target entity fields.<br><strong>Note:</strong> For \"User Reference\" mappings, the search attribute will be automatically obtained from the \"Account name attribute\" field configured in the selected LDAP server."
msgstr "Configure como os atributos LDAP são mapeados para os campos da entidade alvo.<br><strong>Nota:</strong> Para mapeamentos do tipo \"Referência de Usuário\", o atributo de busca será obtido automaticamente do campo \"Atributo de nome de conta\" configurado no servidor LDAP selecionado."
msgid "Entity Field"
msgstr "Campo da Entidade"
msgid "LDAP Attribute"
msgstr "Atributo LDAP"
msgid "Mapping Type"
msgstr "Tipo de Mapeamento"
msgid "Remove"
msgstr "Remover"
msgid "No mappings configured."
msgstr "Nenhum mapeamento configurado."
msgid "- Select a field -"
msgstr "- Selecione um campo -"
msgid "- Select an attribute -"
msgstr "- Selecione um atributo -"
msgid "Simple (text)"
msgstr "Simples (texto)"
msgid "User Reference (DN → User)"
msgstr "Referência de Usuário (DN → Usuário)"
msgid "Department Reference (code → Department)"
msgstr "Referência de Departamento (código → Departamento)"
msgid "Add Mapping"
msgstr "Adicionar Mapeamento"
msgid "Remove Selected"
msgstr "Remover Selecionados"
msgid "Member Synchronization"
msgstr "Sincronização de Membros"
msgid "Enable member synchronization from LDAP attribute"
msgstr "Habilitar sincronização de membros a partir do atributo LDAP"
msgid "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."
msgstr "Quando habilitado, as associações de membros do grupo são gerenciadas com base no atributo de membros LDAP de cada entrada de grupo. Membros são adicionados ou removidos para corresponder à lista LDAP."
msgid "Member LDAP attribute"
msgstr "Atributo LDAP de membros"
msgid "Name of the LDAP attribute on each group entry that lists its members (as DNs or UIDs). Common values: <code>member</code>, <code>uniqueMember</code>, <code>memberUid</code>. The attribute must be included in the LDAP query."
msgstr "Nome do atributo LDAP em cada entrada de grupo que lista seus membros (como DNs ou UIDs). Valores comuns: <code>member</code>, <code>uniqueMember</code>, <code>memberUid</code>. O atributo deve estar incluído na query LDAP."
msgid "Group Role Mapping"
msgstr "Mapeamento de Papéis do Grupo"
msgid "Configure how user attributes map to group roles. Each role can have its own mapping criteria."
msgstr "Configure como os atributos do usuário são mapeados para papéis do grupo. Cada papel pode ter seus próprios critérios de mapeamento."
msgid "Enable role mapping"
msgstr "Habilitar mapeamento de papéis"
msgid "When enabled, users will be assigned group roles based on the mappings below."
msgstr "Quando habilitado, os usuários receberão papéis de grupo com base nos mapeamentos abaixo."
msgid "Group Role"
msgstr "Papel do Grupo"
msgid "Source"
msgstr "Fonte"
msgid "User Field / LDAP Attribute"
msgstr "Campo do Usuário / Atributo LDAP"
msgid "Group Field / Values"
msgstr "Campo do Grupo / Valores"
msgid "No role mappings configured. Users will be assigned the default role."
msgstr "Nenhum mapeamento de papel configurado. Os usuários receberão o papel padrão."
msgid "- Select a role -"
msgstr "- Selecione um papel -"
msgid "User Field"
msgstr "Campo do Usuário"
msgid "LDAP Attribute"
msgstr "Atributo LDAP"
msgid "Group Field Match"
msgstr "Correspondência de Campo do Grupo"
msgid "- Select user field -"
msgstr "- Selecione campo do usuário -"
msgid "- Select attribute/field -"
msgstr "- Selecione atributo/campo -"
msgid "- Select group field -"
msgstr "- Selecione campo do grupo -"
msgid "e.g., Director, Manager"
msgstr "ex: Diretor, Gerente"
msgid "Add Role Mapping"
msgstr "Adicionar Mapeamento de Papel"
msgid "Save Configuration"
msgstr "Salvar Configuração"
msgid "Test Connection"
msgstr "Testar Conexão"
msgid "Synchronize Research Groups"
msgstr "Sincronizar Grupos de Pesquisa"
msgid "Error loading LDAP servers: @message"
msgstr "Erro ao carregar servidores LDAP: @message"
msgid "Error loading query attributes: @message"
msgstr "Erro ao carregar atributos da query: @message"
msgid "LDAP attribute is required when field is selected."
msgstr "O atributo LDAP é obrigatório quando um campo é selecionado."
msgid "Vocabulary field is required when attribute is selected."
msgstr "O campo vocabulário é obrigatório quando um atributo é selecionado."
msgid "Configuration saved successfully."
msgstr "Configuração salva com sucesso."
msgid "Please select an LDAP server to test."
msgstr "Por favor, selecione um servidor LDAP para testar."
msgid "LDAP server \"@server\" is configured and active."
msgstr "Servidor LDAP \"@server\" está configurado e ativo."
msgid "LDAP server not found or inactive."
msgstr "Servidor LDAP não encontrado ou inativo."
msgid "Error testing connection: @message"
msgstr "Erro ao testar conexão: @message"
msgid "Please fix the errors in the form before running the synchronization."
msgstr "Corrija os erros no formulário antes de executar a sincronização."
msgid "Synchronization completed successfully. Check the logs for details."
msgstr "Sincronização executada com sucesso. Verifique os logs para detalhes."
msgid "Error during synchronization: @message"
msgstr "Erro durante a sincronização: @message"
msgid "Internal"
msgstr "Interno"
msgid "Outsider"
msgstr "Externo"
msgid "Error loading group roles: @message"
msgstr "Erro ao carregar papéis do grupo: @message"
msgid "Error loading user fields: @message"
msgstr "Erro ao carregar campos do usuário: @message"
msgid "The ldap_query module is not available. LDAP queries cannot be loaded."
msgstr "O módulo ldap_query não está disponível. As queries LDAP não podem ser carregadas."
msgid "Error loading LDAP queries: @message"
msgstr "Erro ao carregar queries LDAP: @message"
msgid "Label (group title)"
msgstr "Rótulo (título do grupo)"
msgid "Group type \"@type\" not found. Please create it first or select a different group type."
msgstr "Tipo de grupo \"@type\" não encontrado. Crie-o primeiro ou selecione um tipo de grupo diferente."
msgid "Error loading group types: @message"
msgstr "Erro ao carregar tipos de grupo: @message"
msgid "Configuration directory not found: @path"
msgstr "Diretório de configuração não encontrado: @path"
msgid "Configuration @name already exists, skipping."
msgstr "Configuração @name já existe, pulando."
msgid "Successfully imported @count configuration files."
msgstr "Importados com sucesso @count arquivos de configuração."
msgid "No new configuration files were imported. All configurations already exist."
msgstr "Nenhum arquivo de configuração novo foi importado. Todas as configurações já existem."
msgid "Error importing configuration: @message"
msgstr "Erro ao importar configuração: @message"