From 6266b42e0e01138a9985056e6c545984fdf6f1f3 Mon Sep 17 00:00:00 2001 From: "Quintino A. G. Souza" Date: Wed, 25 Feb 2026 15:23:20 -0300 Subject: [PATCH] feat: Sincroniza foto LDAP para field_user_photos no login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa hook_ldap_user_edit_user_alter() para capturar a foto do atributo LDAP configurado e adicioná-la como primeira entrada em field_user_photos, sem queries adicionais ao servidor. Inclui LdapPhotoSyncService com detecção de tipo via exif_imagetype, deduplicação por MD5 e reutilização de media entity existente. Adiciona checkbox para ativar/desativar o sync no formulário de settings, com visibilidade condicional do campo de atributo via #states. Corrige acesso a mídias publicadas para usuários autenticados via hook_media_access(), resolvendo "Acesso restrito" no widget e na visualização do perfil. Co-Authored-By: Claude Sonnet 4.6 --- config/install/site_users.settings.yml | 1 + site_users.module | 20 +++ site_users.services.yml | 8 ++ src/Form/SiteUsersSettingsForm.php | 15 ++- src/Service/LdapPhotoSyncService.php | 167 +++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 site_users.services.yml create mode 100644 src/Service/LdapPhotoSyncService.php diff --git a/config/install/site_users.settings.yml b/config/install/site_users.settings.yml index 41cebde..df9b794 100644 --- a/config/install/site_users.settings.yml +++ b/config/install/site_users.settings.yml @@ -1,6 +1,7 @@ photos: max_count: 5 ldap_attribute: 'jpegPhoto' + ldap_sync_enabled: false user_editable_fields: field_user_name: true field_user_phone: true diff --git a/site_users.module b/site_users.module index ff82b99..b3af6e1 100644 --- a/site_users.module +++ b/site_users.module @@ -17,6 +17,7 @@ use Drupal\field\FieldConfigInterface; use Drupal\media\MediaInterface; use Drupal\site_users\Plugin\Field\FieldType\SocialLinkItem; use Drupal\user\UserInterface; +use Symfony\Component\Ldap\Entry; /** * Implements hook_theme(). @@ -331,6 +332,25 @@ function site_users_user_presave(UserInterface $user) { } } +/** + * Implements hook_ENTITY_TYPE_access() for media entities. + */ +function site_users_media_access(\Drupal\media\MediaInterface $entity, string $operation, AccountInterface $account): AccessResult { + if ($operation === 'view' && $entity->isPublished() && $account->isAuthenticated()) { + return AccessResult::allowed() + ->cachePerUser() + ->addCacheableDependency($entity); + } + return AccessResult::neutral(); +} + +/** + * Implements hook_ldap_user_edit_user_alter(). + */ +function site_users_ldap_user_edit_user_alter(UserInterface &$account, Entry $ldapEntry, array $context): void { + \Drupal::service('site_users.ldap_photo_sync')->syncFromLdapEntry($account, $ldapEntry); +} + /** * Implements hook_user_format_name_alter(). */ diff --git a/site_users.services.yml b/site_users.services.yml new file mode 100644 index 0000000..459b64e --- /dev/null +++ b/site_users.services.yml @@ -0,0 +1,8 @@ +services: + site_users.ldap_photo_sync: + class: Drupal\site_users\Service\LdapPhotoSyncService + arguments: + - '@config.factory' + - '@entity_type.manager' + - '@file.repository' + - '@file_system' diff --git a/src/Form/SiteUsersSettingsForm.php b/src/Form/SiteUsersSettingsForm.php index 48e9163..c0a2b56 100644 --- a/src/Form/SiteUsersSettingsForm.php +++ b/src/Form/SiteUsersSettingsForm.php @@ -98,12 +98,24 @@ class SiteUsersSettingsForm extends ConfigFormBase { '#required' => TRUE, ]; + $form['photos']['photos_ldap_sync_enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable LDAP photo synchronization'), + '#description' => $this->t('When enabled, the user photo from LDAP is automatically added as the first profile photo on each login.'), + '#default_value' => $config->get('photos.ldap_sync_enabled') ?? FALSE, + ]; + $form['photos']['photos_ldap_attribute'] = [ '#type' => 'textfield', '#title' => $this->t('LDAP photo attribute'), - '#description' => $this->t('If LDAP is enabled, enter the name of the attribute that contains the user photo (e.g., thumbnailPhoto, jpegPhoto).'), + '#description' => $this->t('Name of the LDAP attribute that contains the user photo (e.g., thumbnailPhoto, jpegPhoto).'), '#default_value' => $config->get('photos.ldap_attribute') ?? 'jpegPhoto', '#maxlength' => 255, + '#states' => [ + 'visible' => [ + ':input[name="photos_ldap_sync_enabled"]' => ['checked' => TRUE], + ], + ], ]; // Fieldset para campos editáveis pelo próprio usuário. @@ -144,6 +156,7 @@ class SiteUsersSettingsForm extends ConfigFormBase { $config ->set('photos.max_count', $form_state->getValue('photos_max_count')) + ->set('photos.ldap_sync_enabled', (bool) $form_state->getValue('photos_ldap_sync_enabled')) ->set('photos.ldap_attribute', $form_state->getValue('photos_ldap_attribute')); $definitions = \Drupal::service('entity_field.manager') diff --git a/src/Service/LdapPhotoSyncService.php b/src/Service/LdapPhotoSyncService.php new file mode 100644 index 0000000..bcb786a --- /dev/null +++ b/src/Service/LdapPhotoSyncService.php @@ -0,0 +1,167 @@ +configFactory->get('site_users.settings'); + + if (!$config->get('photos.ldap_sync_enabled')) { + return; + } + + $attribute = $config->get('photos.ldap_attribute'); + if (!$attribute || !$ldapEntry->hasAttribute($attribute, FALSE)) { + return; + } + + $binary = $ldapEntry->getAttribute($attribute, FALSE)[0] ?? NULL; + if (empty($binary)) { + return; + } + + $extension = $this->detectExtension($binary); + if (!$extension) { + return; + } + + $directory = 'public://ldap_photos'; + $uri = $directory . '/' . 'ldap_photo_' . $account->id() . '.' . $extension; + + // If file exists and content is unchanged, just ensure it is first. + $realpath = $this->fileSystem->realpath($uri); + if ($realpath && file_exists($realpath) && md5_file($realpath) === md5($binary)) { + $this->ensureMediaFirstInField($account, $uri); + return; + } + + $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + + $file = $this->fileRepository->writeData($binary, $uri, FileExists::Replace); + if (!$file) { + return; + } + + $media = $this->findOrCreateMedia($file, (int) $account->id()); + if (!$media) { + return; + } + + $this->prependMediaToField($account, (int) $media->id()); + } + + /** + * Detects the image extension from binary data via a temporary file. + */ + private function detectExtension(string $binary): ?string { + $tmp = $this->fileSystem->tempnam('temporary://', 'ldap_photo_'); + file_put_contents($this->fileSystem->realpath($tmp), $binary); + $type = @exif_imagetype($this->fileSystem->realpath($tmp)); + $this->fileSystem->unlink($tmp); + + return match ($type) { + IMAGETYPE_JPEG => 'jpg', + IMAGETYPE_PNG => 'png', + IMAGETYPE_GIF => 'gif', + default => NULL, + }; + } + + /** + * Returns an existing media entity for the file, or creates one. + */ + private function findOrCreateMedia(FileInterface $file, int $uid): ?object { + $media_storage = $this->entityTypeManager->getStorage('media'); + + $existing = $media_storage->loadByProperties([ + 'bundle' => 'image', + 'field_media_image.target_id' => $file->id(), + ]); + + if (!empty($existing)) { + return reset($existing); + } + + $media = $media_storage->create([ + 'bundle' => 'image', + 'uid' => $uid, + 'name' => $file->getFilename(), + 'field_media_image' => [ + 'target_id' => $file->id(), + 'alt' => $file->getFilename(), + ], + 'status' => 1, + ]); + $media->save(); + + return $media; + } + + /** + * Ensures the media for an existing file URI is first in field_user_photos. + */ + private function ensureMediaFirstInField(UserInterface $account, string $uri): void { + $files = $this->entityTypeManager->getStorage('file') + ->loadByProperties(['uri' => $uri]); + if (empty($files)) { + return; + } + + $file = reset($files); + $media_storage = $this->entityTypeManager->getStorage('media'); + $existing = $media_storage->loadByProperties([ + 'bundle' => 'image', + 'field_media_image.target_id' => $file->id(), + ]); + if (empty($existing)) { + return; + } + + $media = reset($existing); + $this->prependMediaToField($account, (int) $media->id()); + } + + /** + * Prepends a media entity to field_user_photos, removing any duplicate. + */ + private function prependMediaToField(UserInterface $account, int $media_id): void { + $current_ids = array_column($account->get('field_user_photos')->getValue(), 'target_id'); + + if (!empty($current_ids) && (int) $current_ids[0] === $media_id) { + return; + } + + $filtered = array_values(array_filter($current_ids, fn($id) => (int) $id !== $media_id)); + array_unshift($filtered, $media_id); + $account->set('field_user_photos', array_map(fn($id) => ['target_id' => $id], $filtered)); + } + +}