mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/site_users.git
synced 2026-03-08 01:17:41 -03:00
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:
@@ -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
|
||||
|
||||
@@ -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
8
site_users.services.yml
Normal 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'
|
||||
@@ -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')
|
||||
|
||||
167
src/Service/LdapPhotoSyncService.php
Normal file
167
src/Service/LdapPhotoSyncService.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user