feat: Sincroniza foto LDAP para field_user_photos no login

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 15:23:20 -03:00
parent 5cacb97f12
commit 6266b42e0e
5 changed files with 210 additions and 1 deletions

View File

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

View File

@@ -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().
*/

8
site_users.services.yml Normal file
View File

@@ -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'

View File

@@ -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')

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Drupal\site_users\Service;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\FileInterface;
use Drupal\file\FileRepositoryInterface;
use Drupal\user\UserInterface;
use Symfony\Component\Ldap\Entry;
/**
* Synchronizes the user photo from an LDAP entry to field_user_photos.
*/
class LdapPhotoSyncService {
public function __construct(
private ConfigFactoryInterface $configFactory,
private EntityTypeManagerInterface $entityTypeManager,
private FileRepositoryInterface $fileRepository,
private FileSystemInterface $fileSystem,
) {}
/**
* Syncs the LDAP photo to the user's field_user_photos.
*
* The photo is always placed first in the list. If the photo has not
* changed since the last sync (same MD5), no file or media write occurs.
*/
public function syncFromLdapEntry(UserInterface $account, Entry $ldapEntry): void {
$config = $this->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));
}
}