Files
site_users/site_users.module
Quintino A. G. Souza bd24e6eb6b feat: View mode por role/usuário com seletor de visibilidade no perfil
- Cria view mode e display 'restricted' (template mostra só username + mensagem)
- Adiciona campo field_user_selected_view_mode (string, default: restricted)
- update_10006: provisiona view mode, display e campo; inicializa role_view_modes
- Seletor de visibilidade no form de edição do perfil (owner e admin)
- hook_entity_view_mode_alter lê o campo e valida existência do display
- Formulário de admin: checkboxes de view modes por role (exceto anonymous e authenticated)
- Schema YAML completo para site_users.settings incluindo role_view_modes
- Tradução pt-BR de todas as novas strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 08:06:10 -03:00

603 lines
19 KiB
Plaintext

<?php
/**
* @file
* Módulo Site Users - customizações de usuários do site.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
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().
*/
function site_users_theme($existing, $type, $theme, $path) {
return [
'user__full' => [
'template' => 'user--full',
'base hook' => 'user',
],
'user__restricted' => [
'template' => 'user--restricted',
'base hook' => 'user',
],
'site_users_info_block' => [
'template' => 'site-user-info-block',
'variables' => [
'user_info' => [],
'user' => NULL,
],
],
];
}
/**
* Implements hook_theme_suggestions_HOOK_alter() for user templates.
*/
function site_users_theme_suggestions_user_alter(array &$suggestions, array $variables) {
$view_mode = $variables['elements']['#view_mode'] ?? 'default';
$suggestions[] = 'user__' . $view_mode;
}
/**
* Implements hook_entity_field_access().
*/
function site_users_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
// Apenas para entidade user.
if ($field_definition->getTargetEntityTypeId() !== 'user') {
return AccessResult::neutral();
}
// Apenas campos configuráveis (não campos base como name, mail, status).
if (!($field_definition instanceof FieldConfigInterface)) {
return AccessResult::neutral();
}
$field_name = $field_definition->getName();
// Campos de referência a mídia usam lógica de fotos.
if ($field_definition->getType() === 'entity_reference'
&& $field_definition->getSetting('target_type') === 'media') {
return site_users_check_photo_field_access($operation, $account, $items, $field_name);
}
return site_users_check_profile_field_access($operation, $account, $items, $field_name);
}
/**
* Verifica acesso aos campos de perfil.
*/
function site_users_check_profile_field_access($operation, AccountInterface $account, ?FieldItemListInterface $items = NULL, string $field_name = '') {
// Administradores têm acesso total.
if ($account->hasPermission('administer site_users settings')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Determinar se é o próprio usuário.
$is_own = FALSE;
if ($items) {
$entity = $items->getEntity();
$is_own = ($entity->id() == $account->id());
}
if ($operation === 'view') {
if ($account->hasPermission('view any user profile fields')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Sem entidade disponível, defer.
if (!$items) {
return AccessResult::neutral();
}
if ($is_own && $account->hasPermission('view own user profile fields')) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser();
}
// Visibilidade pública controlada pelo view mode — não bloquear aqui.
return AccessResult::neutral()->cachePerUser();
}
if ($operation === 'edit') {
if ($account->hasPermission('edit any user profile fields')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Sem entidade disponível, defer.
if (!$items) {
return AccessResult::neutral();
}
if ($is_own) {
// Campo habilitado na config?
$config = \Drupal::config('site_users.settings');
$field_enabled = $config->get('user_editable_fields.' . $field_name) ?? TRUE;
if ($field_enabled) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheTags(['config:site_users.settings']);
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser()->addCacheTags(['config:site_users.settings']);
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser();
}
return AccessResult::neutral();
}
/**
* Verifica acesso ao campo de fotos.
*/
function site_users_check_photo_field_access($operation, AccountInterface $account, ?FieldItemListInterface $items = NULL, string $field_name = '') {
// Administradores têm acesso total.
if ($account->hasPermission('administer site_users settings')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Determinar se é o próprio usuário.
$is_own = FALSE;
if ($items) {
$entity = $items->getEntity();
$is_own = ($entity->id() == $account->id());
}
if ($operation === 'view') {
if ($account->hasPermission('view any user profile fields')) {
return AccessResult::allowed()->cachePerPermissions();
}
if (!$items) {
return AccessResult::neutral();
}
if ($is_own && $account->hasPermission('view own user profile fields')) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser();
}
// Visibilidade pública controlada pelo view mode — não bloquear aqui.
return AccessResult::neutral()->cachePerUser();
}
if ($operation === 'edit') {
if ($account->hasPermission('manage user photos')) {
return AccessResult::allowed()->cachePerPermissions();
}
if (!$items) {
return AccessResult::neutral();
}
if ($is_own) {
$config = \Drupal::config('site_users.settings');
$field_enabled = $config->get('user_editable_fields.' . $field_name) ?? TRUE;
if ($field_enabled) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheTags(['config:site_users.settings']);
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser()->addCacheTags(['config:site_users.settings']);
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser();
}
return AccessResult::neutral();
}
/**
* Implements hook_form_FORM_ID_alter() for user_form.
*/
function site_users_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (isset($form['field_user_photos'])) {
$form['#validate'][] = 'site_users_validate_photos_count';
_site_users_add_default_photo_selector($form, $form_state);
}
_site_users_add_visibility_selector($form, $form_state);
}
/**
* Adiciona o seletor de visibilidade do perfil ao formulário de usuário.
*/
function _site_users_add_visibility_selector(&$form, FormStateInterface $form_state) {
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
$current_user = \Drupal::currentUser();
$is_own = (int) $user->id() === (int) $current_user->id();
$is_admin = $current_user->hasPermission('administer users');
if (!$is_own && !$is_admin) {
return;
}
$options = _site_users_get_allowed_view_modes($user);
// Always include 'restricted'.
$options = ['restricted' => t('Restricted (username only)')] + $options;
$current_value = 'restricted';
if ($user->hasField('field_user_selected_view_mode') && !$user->get('field_user_selected_view_mode')->isEmpty()) {
$current_value = $user->get('field_user_selected_view_mode')->value;
}
// Fallback to restricted if stored value is no longer allowed.
if (!isset($options[$current_value])) {
$current_value = 'restricted';
}
$form['profile_visibility'] = [
'#type' => 'fieldset',
'#title' => t('Profile visibility'),
'#weight' => 20,
];
$form['profile_visibility']['profile_view_mode'] = [
'#type' => 'radios',
'#title' => t('How should my profile appear to other visitors?'),
'#description' => t('Choose the information displayed on your public profile page.'),
'#options' => $options,
'#default_value' => $current_value,
];
// Prepend validate handler.
array_unshift($form['#validate'], '_site_users_validate_visibility_selector');
// Prepend submit handler before the default submit.
array_unshift($form['actions']['submit']['#submit'], '_site_users_presave_visibility_selector');
}
/**
* Retorna os view modes permitidos para os roles do usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário cujo perfil está sendo editado.
*
* @return array
* Associative array of machine_name => label.
*/
function _site_users_get_allowed_view_modes(UserInterface $user): array {
$config = \Drupal::config('site_users.settings');
$role_view_modes = $config->get('role_view_modes') ?? [];
$allowed = [];
foreach ($user->getRoles() as $role_id) {
if (!empty($role_view_modes[$role_id]) && is_array($role_view_modes[$role_id])) {
foreach ($role_view_modes[$role_id] as $vm) {
$allowed[$vm] = $vm;
}
}
}
if (empty($allowed)) {
return [];
}
// Load labels for the allowed view modes.
$view_mode_storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode');
$options = [];
foreach (array_keys($allowed) as $machine_name) {
$view_mode = $view_mode_storage->load('user.' . $machine_name);
$options[$machine_name] = $view_mode ? $view_mode->label() : $machine_name;
}
return $options;
}
/**
* Validate handler: confirma que o valor escolhido está na lista permitida.
*/
function _site_users_validate_visibility_selector(&$form, FormStateInterface $form_state) {
$chosen = $form_state->getValue('profile_view_mode');
if ($chosen === NULL) {
return;
}
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
$allowed = _site_users_get_allowed_view_modes($user);
$allowed['restricted'] = 'restricted';
if (!isset($allowed[$chosen])) {
$form_state->setErrorByName('profile_view_mode', t('Invalid profile visibility option selected.'));
}
}
/**
* Submit handler (prepended): seta field_user_selected_view_mode no entity antes do save.
*/
function _site_users_presave_visibility_selector(&$form, FormStateInterface $form_state) {
$chosen = $form_state->getValue('profile_view_mode');
if ($chosen === NULL) {
return;
}
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
if ($user->hasField('field_user_selected_view_mode')) {
$user->set('field_user_selected_view_mode', $chosen);
// Do not save here — the standard submit handler saves the entity.
}
}
/**
* Implements hook_form_FORM_ID_alter() for user_register_form.
*/
function site_users_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (isset($form['field_user_photos'])) {
$form['#validate'][] = 'site_users_validate_photos_count';
_site_users_add_default_photo_selector($form, $form_state);
}
}
/**
* Adiciona o seletor de foto padrão ao formulário.
*/
function _site_users_add_default_photo_selector(&$form, FormStateInterface $form_state) {
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
// Obter fotos atuais do usuário.
$photos = [];
if ($user->hasField('field_user_photos') && !$user->get('field_user_photos')->isEmpty()) {
foreach ($user->get('field_user_photos')->referencedEntities() as $media) {
if ($media instanceof MediaInterface) {
$photos[$media->id()] = $media->label();
}
}
}
// Obter foto padrão atual.
$default_photo_id = NULL;
if ($user->hasField('field_user_default_photo') && !$user->get('field_user_default_photo')->isEmpty()) {
$default_photo_id = $user->get('field_user_default_photo')->target_id;
}
// Adicionar seletor de foto padrão após o campo de fotos.
$form['default_photo_selector'] = [
'#type' => 'container',
'#weight' => $form['field_user_photos']['#weight'] + 0.5 ?? 15,
'#states' => [
'visible' => [
':input[name="field_user_photos[selection]"]' => ['filled' => TRUE],
],
],
];
if (!empty($photos)) {
$form['default_photo_selector']['field_user_default_photo_select'] = [
'#type' => 'radios',
'#title' => t('Default photo'),
'#description' => t('Select the main profile photo.'),
'#options' => $photos,
'#default_value' => $default_photo_id,
];
}
else {
$form['default_photo_selector']['field_user_default_photo_select'] = [
'#type' => 'item',
'#title' => t('Default photo'),
'#markup' => t('Add photos to select one as default.'),
];
}
// Adicionar submit handler para salvar a foto padrão.
$form['actions']['submit']['#submit'][] = '_site_users_save_default_photo';
}
/**
* Submit handler para salvar a foto padrão selecionada.
*/
function _site_users_save_default_photo(&$form, FormStateInterface $form_state) {
$selected_photo = $form_state->getValue('field_user_default_photo_select');
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
if ($user->hasField('field_user_default_photo')) {
$user->set('field_user_default_photo', $selected_photo);
$user->save();
}
}
/**
* Validação customizada para quantidade máxima de fotos.
*/
function site_users_validate_photos_count(&$form, FormStateInterface $form_state) {
$photos = $form_state->getValue('field_user_photos');
if (empty($photos)) {
return;
}
// Contar fotos selecionadas (ignorar valores vazios).
$count = 0;
foreach ($photos as $delta => $photo) {
if (is_numeric($delta) && !empty($photo['target_id'])) {
$count++;
}
}
// Obter limite configurado.
$config = \Drupal::config('site_users.settings');
$max_count = $config->get('photos.max_count') ?? 5;
if ($count > $max_count) {
$form_state->setErrorByName('field_user_photos', t('You can add a maximum of @max photos. Currently @count photos are selected.', [
'@max' => $max_count,
'@count' => $count,
]));
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for user entities.
*/
function site_users_user_presave(UserInterface $user) {
// Garantir consistência da foto padrão.
if (!$user->hasField('field_user_photos') || !$user->hasField('field_user_default_photo')) {
return;
}
// Obter IDs das fotos atuais.
$photo_ids = [];
foreach ($user->get('field_user_photos')->getValue() as $item) {
if (!empty($item['target_id'])) {
$photo_ids[] = $item['target_id'];
}
}
// Obter foto padrão atual.
$default_photo_id = $user->get('field_user_default_photo')->target_id;
// Se não há fotos, limpar foto padrão.
if (empty($photo_ids)) {
$user->set('field_user_default_photo', NULL);
return;
}
// Se a foto padrão não está entre as fotos, selecionar a primeira.
if (empty($default_photo_id) || !in_array($default_photo_id, $photo_ids)) {
$user->set('field_user_default_photo', $photo_ids[0]);
}
}
/**
* Implements hook_entity_view_mode_alter().
*/
function site_users_entity_view_mode_alter(string &$view_mode, EntityInterface $entity, ?array $context): void {
if ($entity->getEntityTypeId() !== 'user' || $view_mode !== 'full') {
return;
}
$current_user = \Drupal::currentUser();
$is_own = (int) $entity->id() === (int) $current_user->id();
$is_admin = $current_user->hasPermission('administer users');
if ($is_own || $is_admin) {
// Owner and admin always see the full view mode.
return;
}
// Read the chosen view mode from the profile being visited.
$chosen = 'restricted';
if ($entity instanceof UserInterface
&& $entity->hasField('field_user_selected_view_mode')
&& !$entity->get('field_user_selected_view_mode')->isEmpty()
) {
$chosen = $entity->get('field_user_selected_view_mode')->value;
}
// Validate that a view display exists for the chosen value.
if ($chosen !== 'restricted') {
$display_id = 'user.user.' . $chosen;
$display_storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
if (!$display_storage->load($display_id)) {
$chosen = 'restricted';
}
}
$view_mode = $chosen;
}
/**
* 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()) {
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().
*/
function site_users_user_format_name_alter(&$name, $account) {
if (!($account instanceof UserInterface)) {
return;
}
if ($account->hasField('field_user_name') && !$account->get('field_user_name')->isEmpty()) {
$name = $account->get('field_user_name')->value;
}
}
/**
* Implements hook_site_tools_share_links().
*/
function site_users_site_tools_share_links(): array {
$user = \Drupal::routeMatch()->getParameter('user');
if (!($user instanceof UserInterface)) {
return [];
}
if (!$user->hasField('field_user_social_links') || $user->get('field_user_social_links')->isEmpty()) {
return [];
}
$links = [];
$networks = SocialLinkItem::getNetworks();
foreach ($user->get('field_user_social_links') as $delta => $item) {
if ($item->isEmpty()) {
continue;
}
$network_label = $networks[$item->network] ?? $item->network;
$links['social_' . $item->network] = [
'content' => [
'#type' => 'link',
'#title' => $network_label,
'#url' => Url::fromUri($item->url),
'#attributes' => [
'class' => ['social-link', 'social-link--' . $item->network],
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
],
'weight' => $delta,
'provider' => 'site_users',
];
}
return $links;
}
/**
* Obtém a foto padrão de um usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário.
*
* @return \Drupal\media\MediaInterface|null
* A entidade de mídia da foto padrão, ou NULL se não houver.
*/
function site_users_get_default_photo(UserInterface $user): ?MediaInterface {
if (!$user->hasField('field_user_default_photo') || !$user->hasField('field_user_photos')) {
return NULL;
}
// Tentar obter a foto padrão configurada.
if (!$user->get('field_user_default_photo')->isEmpty()) {
$default_photo = $user->get('field_user_default_photo')->entity;
if ($default_photo instanceof MediaInterface) {
return $default_photo;
}
}
// Fallback: retornar a primeira foto disponível.
if (!$user->get('field_user_photos')->isEmpty()) {
$first_photo = $user->get('field_user_photos')->first()->entity;
if ($first_photo instanceof MediaInterface) {
return $first_photo;
}
}
return NULL;
}