contacts-8.x-1.x-dev/contacts.module
contacts.module
<?php
/**
* @file
* Module related hook implementations for the contacts module.
*/
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\commerce_order\Form\OrderForm;
use Drupal\contacts\Dashboard;
use Drupal\contacts\Form\AddContactForm;
use Drupal\contacts\Form\ContactsProfileForm;
use Drupal\contacts\Form\RegisterForm;
use Drupal\contacts\Plugin\Block\ContactsEntity;
use Drupal\contacts\Plugin\DashboardBlockInterface;
use Drupal\contacts\Plugin\Field\EntityLabelItemList;
use Drupal\contacts\Plugin\facets\url_processor\StatefulQuerystringUrlProcessor;
use Drupal\decoupled_auth\DecoupledAuthUserInterface;
use Drupal\search_api\SearchApiException;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Implements hook_theme().
*/
function contacts_theme() {
return [
'contacts_dash_tabs' => [
'variables' => [
'tabs' => [],
'contact' => NULL,
'manage_mode' => NULL,
'attributes' => [],
],
'template' => 'contacts-dash-tabs',
],
'contacts_dash_summary' => [
'variables' => ['user' => NULL],
'template' => 'contacts-dash-summary',
],
'user__contacts_dashboard_row' => [
'base hook' => 'user',
'template' => 'user--contacts-dashboard-row',
],
'user__contacts_dashboard_row_small' => [
'base hook' => 'user',
'template' => 'user--contacts-dashboard-row-small',
],
'contacts_manage_block' => [
'variables' => [
'id' => NULL,
'tab' => NULL,
'subpage' => NULL,
'block' => [],
'attributes' => [],
'label' => '',
'description' => '',
],
'template' => 'contacts-manage-block',
],
// Override the rendering of the dashboard tabs block to remove
// block markup applied by the theme.
'block__tabs__contacts_dashboard' => [
'template' => 'block--tabs--contacts-dashboard',
'base hook' => 'block',
],
];
}
/**
* Prepares variables for contacts manage blocks.
*
* Default template: contacts-manage-block.html.twig.
*
* @param array $variables
* An associative array containing:
* - attributes: HTML markup attributes for the content wrapper.
* - region_attributes: HTML markup attributes for the content wrapper.
*/
function template_preprocess_contacts_manage_block(array &$variables): void {
$configuration = $variables['block']->getConfiguration();
if (str_contains($variables['id'], PluginBase::DERIVATIVE_SEPARATOR)) {
[$variables['block_plugin'], $variables['identifier']] = explode(PluginBase::DERIVATIVE_SEPARATOR, $variables['id'], 2);
}
else {
$variables['block_plugin'] = $variables['id'];
$variables['identifier'] = NULL;
}
$variables['attributes']['data-contacts-manage-block-name'] = $configuration['name'];
$variables['attributes']['data-contacts-manage-block-tab'] = $variables['tab']->id();
$variables['label'] = $variables['label'] ?: $variables['block']->label(FALSE);
$variables['content']['description'] = [
'#type' => 'html_tag',
'#tag' => 'strong',
'#value' => $variables['description'],
'#prefix' => '<div>',
'#suffix' => '</div>',
];
if ($variables['block'] instanceof DashboardBlockInterface) {
$variables['block']->processManageMode($variables);
}
// UI icons for manage mode.
$variables['ui'] = [
'drag' => [
'#type' => 'open_iconic',
'#size' => '20',
'#icon' => 'move',
'#fill' => '#000',
'#color' => 'transparent',
'#attributes' => ['class' => ['contacts-ui-icon', 'icon-left']],
],
'configure' => [
'#type' => 'open_iconic',
'#size' => '20',
'#icon' => 'cog',
'#fill' => '#5bc0de',
'#color' => 'transparent',
'#attributes' => ['class' => ['contacts-ui-icon', 'icon-right']],
],
];
$variables['attributes'] = new Attribute($variables['attributes']);
}
/**
* Prepares variables for contact dashboard tab headers.
*
* Default template: contacts-dash-tabs.html.twig.
*
* @param array $variables
* An associative array containing the unordered list element
* representing the tab headings. This contains:
* - attributes: HTML markup attributes for the content wrapper.
* - tabs: Associative array of tabs and tab properties.
*/
function template_preprocess_contacts_dash_tabs(array &$variables) {
foreach ($variables['tabs'] as $tab_id => &$tab) {
if ($variables['manage_mode']) {
$tab['drag'] = [
'#type' => 'open_iconic',
'#size' => '15',
'#icon' => 'move',
'#color' => 'transparent',
'#fill' => '#000',
'#attributes' => [
'class' => ['mr-1', 'drag-handle'],
],
];
$tab['edit'] = [
'#type' => 'open_iconic',
'#size' => '15',
'#icon' => 'cog',
'#color' => 'transparent',
'#fill' => '#5bc0de',
'#attributes' => [
'class' => ['use-ajax', 'mr-2'],
'data-ajax-url' => Url::fromRoute('contacts.tab.off_canvas_form',
[
'tab' => $tab_id,
]
)->toString(),
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
];
}
$tab['attributes'] = !empty($tab['attributes']) ? new Attribute($tab['attributes']) : new Attribute();
$tab['link_attributes'] = !empty($tab['link_attributes']) ? new Attribute($tab['link_attributes']) : new Attribute();
}
}
/**
* Prepares variables for contact tab content.
*
* Default template: contact-tab-content.html.twig.
*
* @param array $variables
* An associative array containing:
* - attributes: HTML markup attributes for the content wrapper.
* - region_attributes: HTML markup attributes for the content wrapper.
*/
function template_preprocess_contact_tab_content(array &$variables) {
$variables['region_attributes'] = new Attribute($variables['region_attributes']);
}
/**
* Prepares variables for contact dashboard summary block.
*
* Default template: contacts-dash-summary.html.twig.
*
* @param array $variables
* An associative array containing:
* - attributes: HTML markup attributes for the content wrapper.
* - user: Fully loaded drupal User object.
* - crm_indiv: Boolean whether contact has an indiv profile.
* - crm_org: Boolean whether contact has an org profile.
*/
function template_preprocess_contacts_dash_summary(array &$variables) {
/** @var \Drupal\decoupled_auth\Entity\DecoupledAuthUser $user */
$user = $variables['user'];
$image_display = [
'label' => 'hidden',
'type' => 'image',
'settings' => [
'image_style' => 'contacts_small',
],
];
$default_image_element = [
'#theme' => 'image_style',
'#style_name' => $image_display['settings']['image_style'],
'#width' => 100,
'#height' => 100,
'#attributes' => [],
];
$variables['image'] = $user->hasField('user_picture') && !$user->get('user_picture')->isEmpty() ?
$user->get('user_picture')->get(0)->view($image_display) : '';
if ($user->getEmail()) {
$email = $user->getEmail();
$variables['email'] = Link::fromTextAndUrl($email, Url::fromUri('mailto:' . $email));
}
$variables['name'] = $user->getDisplayName();
$route_name = \Drupal::moduleHandler()->moduleExists('contacts_user_dashboard') ?
'contacts_user_dashboard.summary' :
'entity.user.canonical';
$variables['user_dashboard'] = Link::createFromRoute(new TranslatableMarkup('View user dashboard'), $route_name, ['user' => $user->id()], ['attributes' => ['target' => '_blank']]);
// For individuals.
if ($user->hasRole('crm_indiv')) {
if ($user->hasField('profile_crm_indiv') && $profile = $user->profile_crm_indiv->entity) {
/** @var \Drupal\profile\Entity\ProfileInterface $profile */
if ($profile->hasField('crm_phone') && !$profile->get('crm_phone')->isEmpty()) {
$variables['phone'] = $profile->get('crm_phone')->value;
}
if ($profile->hasField('crm_dob') && !$profile->get('crm_dob')->isEmpty()) {
$variables['dob'] = $profile->get('crm_dob')->first()->view([
'label' => 'hidden',
'settings' => [
'format_type' => 'html_date',
],
]);
}
// If user has a picture on their profile, use this instead
// of the user_picture field.
if ($profile->hasField('crm_photo') && $profile->crm_photo->target_id) {
$variables['image'] = $profile->crm_photo->view($image_display);
}
}
if (empty($variables['image'])) {
$variables['image'] = $default_image_element;
$variables['image']['#uri'] = 'contacts://images/default-indiv.png';
}
}
// For organisations.
elseif ($user->hasRole('crm_org')) {
if ($user->hasField('profile_crm_org') && $profile = $user->profile_crm_org->entity) {
if ($profile->hasField('crm_phone') && !$profile->get('crm_phone')->isEmpty()) {
$variables['phone'] = $profile->crm_phone->value;
}
// If user has an image on their profile, use this instead of the
// user_picture field.
if ($profile->hasField('org_image') && $profile->org_image->target_id) {
$variables['image'] = $profile->org_image->view($image_display);
}
}
if (empty($variables['image'])) {
$variables['image'] = $default_image_element;
$variables['image']['#uri'] = 'contacts://images/default-org.png';
}
}
// Assume this is a web account, so give some indiv like features.
else {
if (empty($variables['image'])) {
$variables['image'] = $default_image_element;
$variables['image']['#uri'] = 'contacts://images/default-indiv.png';
}
}
// Get user roles.
$roles = Role::loadMultiple();
uasort($roles, 'contacts_sort_roles');
$roles = array_intersect(array_keys($roles), $user->getRoles());
$variables['hats'] = [];
foreach ($roles as $role) {
$variables['hats'][] = [
'#theme' => 'crm_tools_hat',
'#role' => $role,
];
}
}
/**
* Implements hook_entity_type_build().
*
* Add contacts dashboard form operation to profile entity.
*/
function contacts_entity_type_build(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
$entity_types['profile']->setFormClass('default', ContactsProfileForm::class);
$entity_types['profile']->setFormClass('contacts_dashboard', ContactsProfileForm::class);
$entity_types['profile']->setFormClass('add_contact', AddContactForm::class);
$entity_types['user']->setFormClass('register', RegisterForm::class);
}
/**
* Implements hook_ENTITY_TYPE_insert() for profile_type.
*
* Set up entity displays for contacts dashboard.
*/
function contacts_profile_type_insert($entity) {
$entity_type_manager = \Drupal::entityTypeManager();
// If we don't have the view mode, don't do anything.
if (!$entity_type_manager->getStorage('entity_view_mode')->load('contacts_dashboard')) {
return;
}
/** @var \Drupal\profile\Entity\ProfileTypeInterface $entity */
$view_storage = $entity_type_manager->getStorage('entity_view_display');
$form_storage = $entity_type_manager->getStorage('entity_form_display');
$values = [
'id' => implode('.', ['profile', $entity->id(), 'contacts_dashboard']),
'targetEntityType' => 'profile',
'bundle' => $entity->id(),
'mode' => 'contacts_dashboard',
'status' => TRUE,
];
if (!$view_storage->load($values['id'])) {
$view_display = $view_storage->create($values);
$view_display->save();
}
if (!$form_storage->load($values['id'])) {
$form_display = $form_storage->create($values);
$form_display->save();
}
}
/**
* Implements hook_ENTITY_TYPE_update() for profile.
*/
function contacts_profile_insert(EntityInterface $entity) {
\Drupal::service('contacts.indexer')->profilePostSave($entity);
}
/**
* Implements hook_ENTITY_TYPE_insert() for profile.
*/
function contacts_profile_update(EntityInterface $entity) {
/** @var \Drupal\profile\Entity\ProfileInterface $entity */
$tags = [];
// Invalidate user cache in case name has changed.
if (in_array($entity->bundle(), ['crm_indiv', 'crm_org'])) {
$tags[] = 'user:' . $entity->getOwnerId();
}
if (!empty($tags)) {
Cache::invalidateTags($tags);
}
\Drupal::service('contacts.indexer')->profilePostSave($entity);
}
/**
* Implements hook_ENTITY_TYPE_delete() for profile.
*/
function contacts_profile_delete(EntityInterface $entity) {
\Drupal::service('contacts.indexer')->profilePostSave($entity);
}
/**
* Implements hook_entity_field_access().
*/
function contacts_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
if ($operation == 'view' && $field_definition->getName() == 'mail' && $field_definition->getTargetEntityTypeId() == 'user') {
return AccessResult::allowedIfHasPermission($account, 'view contacts');
}
return AccessResult::neutral();
}
/**
* Implements hook_user_access().
*/
function contacts_user_access(UserInterface $user, $operation, AccountInterface $account) {
// Allow users to view blocked user accounts if they can view contacts.
// The only other way to view blocked accounts is using `administer users`
// permission which is overkill for some cases.
if ($operation == 'view' && $user->isBlocked()) {
return AccessResult::allowedIfHasPermission($account, 'view contacts');
}
return AccessResult::neutral();
}
/**
* Implements hook_ENTITY_TYPE_insert() for user.
*/
function contacts_user_insert(EntityInterface $entity) {
\Drupal::service('contacts.indexer')->indexImmediately($entity, TRUE);
}
/**
* Implements hook_toolbar().
*
* Add contacts manage icon css to toolbar.
*/
function contacts_toolbar() {
$items = [];
$items['contacts_manage'] = [
'#cache' => [
'contexts' => [
'user.permissions',
'route.name',
],
],
];
if (!\Drupal::service('contacts.dashboard')->isDashboard()) {
return $items;
}
if (!\Drupal::currentUser()->hasPermission('manage contacts dashboard')) {
return $items;
}
$items['contacts_manage'] += [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => t('Manage Dashboard'),
'#attributes' => [
'class' => [
'toolbar-icon',
'toolbar-icon-edit',
'toolbar-dashboard-manage',
],
'aria-pressed' => 'false',
'type' => 'button',
],
],
'tray' => [],
'#wrapper_attributes' => [
'class' => [
'contacts-toolbar-tab',
],
],
'#attached' => [
'library' => [
'contacts/dashboard.manage',
'crm_tools/open-iconic',
'contacts/drag_n_drop',
],
],
];
$path = \Drupal::moduleHandler()->getModule('crm_tools')->getPath();
$items['contacts_manage']['#attached']['drupalSettings']['contacts']['openIconic']['path'] = '/' . $path . '/includes/open-iconic.svg';
return $items;
}
/**
* Implements hook_toolbar_alter().
*
* Add contacts icon css to toolbar.
*/
function contacts_toolbar_alter(&$items) {
// Move the User tab to the right.
$items['administration']['#attached']['library'][] = 'contacts/contacts.toolbar';
}
/**
* Implements hook_user_format_name_alter().
*/
function contacts_user_format_name_alter(&$name, AccountInterface $account) {
// Don't do anything with the anonymous user or new users that haven't
// been saved yet.
if ($account->isAnonymous() || ($account instanceof UserInterface && $account->isNew())) {
return;
}
// Only proceed if we are an individual or organisation and may have a CRM
// based name.
$is_indiv = in_array('crm_indiv', $account->getRoles());
$is_org = in_array('crm_org', $account->getRoles());
if (!$is_indiv && !$is_org) {
return;
}
// Load up the full account as we need to get details from profiles.
// A recent save could result in the profile fields being out of date,
// so always reload.
$account = User::load($account->id());
if (!$account) {
// The user may have been deleted through the Cancel Account screen.
// Drupal will still invoke user_name_format_alter for deleted accounts.
return;
}
// If the user is coupled (i.e. registered), check the config before
// proceeding. If the config is not set, default to enabled..
if ($account->isCoupled() && !(\Drupal::config('contacts.configuration')->get('coupled_real_name') ?? TRUE)) {
return;
}
// Display the name for individuals.
if ($is_indiv && $account->hasField('profile_crm_indiv')) {
$profile_field = $account->get('profile_crm_indiv');
if ($profile_field->count()) {
/** @var \Drupal\profile\Entity\ProfileInterface $profile */
$profile = $profile_field->first()->entity;
if ($profile && $profile->hasField('crm_name') && !$profile->get('crm_name')->isEmpty()) {
$names = [];
/** @var \Drupal\name\Plugin\Field\FieldType\NameItem $name_field */
$name_field = $profile->get('crm_name')->first();
if ($name_field->given) {
$names[] = $name_field->given;
}
if ($name_field->family) {
$names[] = $name_field->family;
}
if (!empty($names)) {
$name = implode(' ', $names);
}
// We have set a name, so don't need to proceed further.
return;
}
}
}
// Display the name for organisations.
if ($is_org && $account->hasField('profile_crm_org')) {
$profile_field = $account->get('profile_crm_org');
if ($profile_field->count()) {
$profile = $profile_field->first()->entity;
if ($profile && $profile->hasField('crm_org_name')) {
$name_field = $profile->get('crm_org_name');
if (!$name_field->isEmpty()) {
$name = $name_field->value;
}
}
}
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function contacts_theme_suggestions_user(array $variables) {
$suggestions = [];
/** @var \Drupal\user\UserInterface $user */
$user = $variables['elements']['#user'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'user__' . $sanitized_view_mode;
foreach ($user->getRoles(TRUE) as $role) {
if (substr($role, 0, 4) == 'crm_') {
$suggestions[] = 'user__' . $role . '__';
$suggestions[] = 'user__' . $role . '__' . $sanitized_view_mode;
}
}
return $suggestions;
}
/**
* Implements hook_preprocess_HOOK().
*/
function contacts_preprocess_user(&$variables) {
$view_mode = $variables['elements']['#view_mode'];
$view_modes = [
'contacts_dashboard_row',
'contacts_dashboard_row_small',
];
if (!in_array($view_mode, $view_modes)) {
return;
}
/** @var \Drupal\user\UserInterface $user */
$user = $variables['user'];
$image_display = [
'label' => 'hidden',
'type' => 'image',
'settings' => [
'image_style' => $view_mode == 'contacts_dashboard_row' ? 'contacts_small' : 'contacts_tiny',
],
];
$default_image_element = [
'#theme' => 'image_style',
'#style_name' => $image_display['settings']['image_style'],
'#width' => 100,
'#height' => 100,
'#attributes' => [],
];
// Set some defaults.
// @todo Simplify this when we have 'primary fields'.
$variables['url'] = Url::fromRoute('contacts.contact', [
'user' => $user->id(),
]);
$variables['label'] = $user->getDisplayName();
$variables['email'] = $user->getEmail();
if ($user instanceof DecoupledAuthUserInterface) {
$variables['web_account'] = $user->isCoupled();
}
$variables['image'] = $user->hasField('user_picture') && !$user->get('user_picture')->isEmpty() ?
$user->get('user_picture')->get(0)->view($image_display) : '';
$variables['address'] = '';
$roles = Role::loadMultiple();
uasort($roles, 'contacts_sort_roles');
$roles = array_map(function ($item) {
return $item->label();
}, $roles);
$variables['roles'] = implode(', ', array_intersect_key($roles, array_fill_keys($user->getRoles(), TRUE)));
// For individuals.
if ($user->hasRole('crm_indiv')) {
if ($profile = $user->profile_crm_indiv->entity) {
$variables['profile'] = $profile;
if ($profile->hasField('crm_address')) {
$variables['address'] = $profile->crm_address->locality ?? NULL;
}
// If user has a picture on their profile, use this instead
// of the user_picture field.
if ($profile->hasField('crm_photo') && $profile->crm_photo->target_id) {
$variables['image'] = $profile->crm_photo->view($image_display);
}
}
if (empty($variables['image'])) {
$variables['image'] = $default_image_element;
$variables['image']['#uri'] = 'contacts://images/default-indiv.png';
}
}
// For organisations.
elseif ($user->hasRole('crm_org')) {
if ($profile = $user->profile_crm_org->entity) {
$variables['profile'] = $profile;
if ($profile->hasField('crm_org_address')) {
$variables['address'] = $profile->crm_org_address->locality ?? NULL;
}
// If user has an image on their profile, use this instead of the
// user_picture field.
if ($profile->hasField('org_image') && $profile->org_image->target_id) {
$variables['image'] = $profile->org_image->view($image_display);
}
}
if (empty($variables['image'])) {
$variables['image'] = $default_image_element;
$variables['image']['#uri'] = 'contacts://images/default-org.png';
}
}
// Assume this is a web account, so give some indiv like features.
else {
if (empty($variables['image'])) {
$variables['image'] = $default_image_element;
$variables['image']['#uri'] = 'contacts://images/default-indiv.png';
}
}
}
/**
* Sort roles by CRM then alphabetically.
*
* @param \Drupal\user\RoleInterface $a
* The first role.
* @param \Drupal\user\RoleInterface $b
* The first role.
*
* @return int
* The comparison result for uasort().
*/
function contacts_sort_roles(RoleInterface $a, RoleInterface $b) {
$a_crm = substr($a->id(), 0, 4) == 'crm_';
$b_crm = substr($b->id(), 0, 4) == 'crm_';
if ($a_crm xor $b_crm) {
return $a_crm ? -1 : 1;
}
return strnatcmp($a->label(), $b->label());
}
/**
* Implements hook_preprocess_HOOK().
*/
function contacts_preprocess_views_view_unformatted(&$variables) {
if ($variables['view']->id() == 'contacts_dashboard_indexed') {
foreach ($variables['rows'] as $id => &$row) {
if (!empty($row['content']['#user'])) {
$url = Url::fromRoute('contacts.contact', [
'user' => $row['content']['#user']->id(),
]);
$row['attributes']->setAttribute('data-row-link', $url->toString());
}
}
}
}
/**
* Implements hook_views_post_render().
*/
function contacts_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
if ($view->id() == 'contacts_dashboard_indexed') {
$output['#attached']['library'][] = 'contacts/listings';
}
}
/**
* Implements hook_entity_type_alter().
*
* Update entity definitions of certain entity types to enable contacts Entity
* blocks and handle missing form operations.
*
* @see \Drupal\contacts\Plugin\Deriver\ContactsEntityBlockDeriver::getDerivativeDefinitions()
*/
function contacts_entity_type_alter(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
// Approved entity types.
$dashboard_entity_types = [
'user',
'profile',
];
/** @var \Drupal\Core\Entity\EntityDisplayRepository $entity_display_repository */
$entity_display_repository = \Drupal::service('entity_display.repository');
foreach ($dashboard_entity_types as $type) {
if (isset($entity_types[$type])) {
$entity_types[$type]->set('contacts_entity', TRUE);
}
// Add form operations for missing form modes.
$form_modes = $entity_display_repository->getFormModes($type);
foreach ($form_modes as $name => $form_mode) {
/** @var \Drupal\Core\Entity\Entity\EntityFormMode $form_mode */
if (!$entity_types[$type]->getFormClass($name)) {
// Use the default form for form modes that are not explicitly defined.
$default_handler_class = $entity_types[$type]->getHandlerClasses()['form']['default'];
$entity_types[$type]->setFormClass($name, $default_handler_class);
}
}
}
// Expand out separate entity blocks for profile bundles.
if (isset($entity_types['profile'])) {
$entity_types['profile']->set('contacts_use_bundles', TRUE);
}
}
/**
* Implements hook_link_alter().
*
* Convert all dashboard links into AJAX links.
*/
function contacts_link_alter(&$variables) {
Dashboard::fastHookLinkAlter($variables, 'contacts.dashboard');
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*
* Point the commerce back end forms to Contacts rather that People.
*/
function contacts_form_commerce_order_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$form_object = $form_state->getFormObject();
if (!($form_object instanceof OrderForm)) {
return;
}
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $form_state->getFormObject()->getEntity();
$customer = $order->getCustomer();
if (!isset($form['uid']) && !$customer->isAnonymous()) {
$customer_link = Link::createFromRoute($customer->label(), 'contacts.contact', ['user' => $customer->id()])->toString();
$label = new TranslatableMarkup('Customer');
$form['customer']['uid'] = [
'#type' => 'item',
'#wrapper_attributes' => [
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
'class' => [Html::cleanCssIdentifier(strtolower($label)), 'container-inline'],
],
'#markup' => '<h4 class="label inline">' . $label . '</h4> ' . $customer_link,
];
}
}
/**
* Implements hook_form_FORM_ID_alter() for user_cancel_form.
*/
function contacts_form_user_cancel_form_alter(&$form, FormStateInterface $form_state, $form_id) {
\Drupal::service('contacts.form_alter.user_cancel_confirm')->alter($form, $form_state, $form_id);
}
/**
* Implements hook_entity_base_field_info().
*/
function contacts_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];
if ($entity_type->id() == 'user') {
$fields['_label'] = BaseFieldDefinition::create('string')
->setName('_label')
->setTargetEntityTypeId('user')
->setLabel(new TranslatableMarkup('Display Name'))
->setRevisionable(FALSE)
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE)
->setReadOnly(TRUE)
->setComputed(TRUE)
->setClass(EntityLabelItemList::class);
}
return $fields;
}
/**
* Implements hook_ENTITY_TYPE_view_alter() for commerce_order.
*/
function contacts_commerce_order_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
// Make the UID link to the contacts dashboard.
if (isset($build['uid'][0]) && \Drupal::currentUser()->hasPermission('view contacts')) {
/** @var \Drupal\user\UserInterface $account */
$account = $build['uid'][0]['#account'];
$build['uid'][0] = Link::createFromRoute($account->getDisplayName(),
'contacts.contact',
['user' => $account->id()])->toRenderable();
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* For form views_exposed_form_contacts_dashboard_indexed_full.
*/
function contacts_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ($form['#id'] == 'views-exposed-form-contacts-dashboard-indexed-simple' || $form['#id'] == 'views-exposed-form-contacts-dashboard-indexed-full') {
// Views exposed form will hide the reset button if there are no filters
// in the querystring. But we allow stateful filters by caching them in
// session. If we have cached filters, then re-enable the button.
$facet_source_id = 'search_api:views_block__contacts_dashboard_indexed__full';
$cache_key = StatefulQuerystringUrlProcessor::getCacheKey($facet_source_id);
if (\Drupal::request()->hasSession()) {
$session = \Drupal::request()->getSession();
if ($session && $session->get($cache_key)) {
$form['actions']['reset']['#access'] = TRUE;
$form['actions']['reset']['#submit'][] = '_contacts_reset_facet_filters';
}
}
// Views exposed form will incorrectly cache the value of the search input.
// Meaning that the list of results will be out of sync with what's rendered
// in the input field. Force the views exposed form to not be cached to
// work around this.
$form['#cache'] = ['max-age' => 0];
}
// Override the views exposed form submission URL for the Individuals and
// Organisations pages.
// Because the individuals/organisation view displays are embeds
// they don't have a URL.
// If we have views_data_export installed and an export is attached
// to these displays then views will incorrectly pick the export URL
// as the form submission action. Override the incorrectly generated urls.
elseif ($form['#id'] == 'views-exposed-form-contacts-dashboard-indexed-individuals') {
$form['#action'] = Url::fromRoute('contacts.collection.individual')->toString();
}
elseif ($form['#id'] == 'views-exposed-form-contacts-dashboard-indexed-organisations') {
$form['#action'] = Url::fromRoute('contacts.collection.organisation')->toString();
}
}
/**
* Resets facet filters.
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*/
function _contacts_reset_facet_filters(array &$form, FormStateInterface $form_state) {
$facet_source_id = 'search_api:views_block__contacts_dashboard_indexed__full';
$cache_key = StatefulQuerystringUrlProcessor::getCacheKey($facet_source_id);
if (\Drupal::request()->hasSession()) {
\Drupal::request()->getSession()->remove($cache_key);
}
}
/**
* Implements hook_form_FORM_ID_alter() for user_pass_reset.
*/
function contacts_form_user_pass_reset_alter(&$form, FormStateInterface $form_state, $form_id) {
if (Drupal::moduleHandler()->moduleExists('email_registration')) {
// By default the reset password form uses the account's account name as
// part of the message "This is a one-time login for %user_name". Usernames
// aren't relevant when we're using email registration, so swap it out for
// the email address.
// @todo Remove once this issue is fixed:
// https://www.drupal.org/project/email_registration/issues/3065842
/** @var \Drupal\Core\StringTranslation\TranslatableMarkup $message */
/** @var \Drupal\Core\Session\AccountInterface $user */
$user = $form_state->getBuildInfo()['args'][0];
$message = $form['message']['#markup'];
$arguments = $message->getArguments();
$arguments['%user_name'] = $user->getEmail();
// Disable coding standards for this line as it doesn't like passing
// non-literal strings to TranslatableMarkup, but that's exactly what we
// want here.
// phpcs:disable Drupal.Semantics.FunctionT.NotLiteralString
$form['message']['#markup'] = new TranslatableMarkup($message->getUntranslatedString(), $arguments);
// phpcs:enable
}
}
/**
* Implements hook_entity_extra_field_info_alter().
*/
function contacts_entity_extra_field_info_alter(&$info) {
/** @var \Drupal\contacts\PseudoEmailField $pseudo_email_field */
$pseudo_email_field = \Drupal::service('contacts.pseudo_email_field');
$pseudo_email_field->extraFieldDefinitions($info);
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for user_form.
*/
function contacts_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\contacts\PseudoEmailField $pseudo_email_field */
// Set up the pseudo email field within the user form, if configured.
$pseudo_email_field = \Drupal::service('contacts.pseudo_email_field');
$pseudo_email_field->userFormAlter($form, $form_state, $form_id);
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_state->get('form_display');
$form['#validate'][] = 'contacts_user_form_validate';
// By default, Drupal will auto-fill user info from cookies (such as username
// email etc). We explicitly don't want this for the user form and its
// derivatives.
unset($form['#attributes']['data-user-info-from-browser']);
// If the password policy module is installed, invoke its form alter but
// not for our AddContactsForm. See contacts_module_implements_alter.
if (function_exists('password_policy_form_user_form_alter')) {
if ($display->id() !== 'user.user.add_contact_organisation' && $display->id() !== 'user.user.add_contact_individual') {
password_policy_form_user_form_alter($form, $form_state);
}
}
}
/**
* Validate callback for the user form.
*
* @param array $form
* Form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* State.
*
* @see contacts_form_user_form_alter
*/
function contacts_user_form_validate(array &$form, FormStateInterface $form_state) {
// Do not allow users to have both the indiv and org roles.
if (isset($form['account']['roles'])) {
$roles = $form_state->getValue('roles') ?? [];
if (in_array('crm_indiv', $roles) && in_array('crm_org', $roles)) {
$role_storage = \Drupal::entityTypeManager()->getStorage('user_role');
/** @var \Drupal\user\Entity\Role $indiv_role */
/** @var \Drupal\user\Entity\Role $org_role */
$indiv_role = $role_storage->load('crm_indiv');
$org_role = $role_storage->load('crm_org');
$form_state->setError($form['account']['roles'], new TranslatableMarkup("A user can't have both the %person and %organisation roles. Please select one or the other, but not both.", [
'%person' => $indiv_role->label(),
'%organisation' => $org_role->label(),
]));
}
}
}
/**
* Implements hook_ENTITY_TYPE_view() for profile.
*/
function contacts_profile_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
// Inject our email pseudo field if it's visible.
/** @var \Drupal\profile\Entity\Profile $entity */
if ($display->getComponent('contacts_mail')) {
/** @var \Drupal\user\UserInterface $user */
if ($user = $entity->getOwner()) {
$mail_field = $user->get('mail');
$build['mail'] = $mail_field->view([
'label' => 'inline',
]);
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for profile_form.
*/
function contacts_form_profile_form_alter(&$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\contacts\PseudoEmailField $pseudo_email_field */
// Set up the pseudo email field within the user form, if configured.
$pseudo_email_field = \Drupal::service('contacts.pseudo_email_field');
$pseudo_email_field->profileFormAlter($form, $form_state, $form_id);
}
/**
* Custom access callback for the restricted account block.
*
* @param \Drupal\contacts\Plugin\Block\ContactsEntity $block
* The block.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param string $op
* The operation being performed.
* @param \Drupal\Core\Session\AccountInterface $account
* The account to check access for.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
function contacts_dashboard_account_form_access(ContactsEntity $block, EntityInterface $entity, string $op, AccountInterface $account) {
// Deny access if the user would have access to the unrestricted version.
return AccessResult::allowedIf(!$entity->access($op, $account));
}
/**
* Prepares variables for views table axes templates.
*
* Default template: views-view-table-axes.html.twig.
*
* @param array $variables
* An associative array containing:
* - view: A ViewExecutable object.
* - rows: The raw row data.
*/
function template_preprocess_views_view_table_axes(array &$variables) {
$view = $variables['view'];
$result = $variables['result'] = $variables['rows'];
$variables['rows'] = [];
$variables['header'] = [];
$options = $view->style_plugin->options;
$handler = $view->style_plugin;
$variables['sticky'] = FALSE;
if (!empty($options['sticky'])) {
$variables['#attached']['library'][] = 'core/drupal.tableheader';
$variables['attributes']['class'][] = 'sticky-enabled';
}
/** @var \Drupal\views\Plugin\views\field\FieldPluginBase[] $fields */
$fields = &$view->field;
// Get our columns.
$variables['header'][$options['row']] = '';
if (isset($options['table_headers'])) {
$variables['header'] += $options['table_headers'];
}
else {
foreach ($result as $num => $row) {
// If this is a dummy row, don't do anything further.
if (!empty($row->_table_axes_dummy)) {
continue;
}
$value = $row->_table_axes_column_value ?? $fields[$options['column']]->getValue($row);
if (!isset($variables['header'][$value])) {
$value = (string) $value;
$variables['header'][$value] = $handler->getField($num, $options['column']);
}
}
}
// Set up our pre headers.
if (isset($options['colgroups'])) {
$variables['colgroups'][$options['row']] = [];
$variables['colgroups'] += $options['colgroups'];
}
// Add our total field.
if (!empty($options['total_field'])) {
$variables['header']['_total'] = t('Total');
if (isset($variables['colgroups'])) {
$variables['colgroups']['_total'] = [];
}
}
// Now loop over and fill out our rows.
foreach ($result as $num => $row) {
// Get our column/row.
$column_value = $row->_table_axes_column_value ?? $fields[$options['column']]->getValue($row);
$row_value = $row->_table_axes_row_value ?? $fields[$options['row']]->getValue($row);
// Ensure that the row and column values can be used as array indexes.
// E.g. casting TranslatableMarkup to strings.
$indexable_column_value = $column_value ? (string) $column_value : (string) $handler->getField($num, $options['column']);
$indexable_row_value = $row_value ? (string) $row_value : (string) $handler->getField($num, $options['row']);
// Build our row.
if (!isset($variables['rows'][$indexable_row_value])) {
$variables['rows'][$indexable_row_value]['data'] = array_fill_keys(array_keys($variables['header']), $options['empty_cell']);
$variables['rows'][$indexable_row_value]['data'][$options['row']] = $row->_table_axes_row_label ?? $handler->getField($num, $options['row']);
if (!empty($row->_table_axes_row_attributes)) {
$variables['rows'][$indexable_row_value]['attributes'] = new Attribute($row->_table_axes_row_attributes);
}
}
// If this is a dummy row, don't do anything further.
if (!empty($row->_table_axes_dummy)) {
continue;
}
$column = &$variables['rows'][$indexable_row_value]['data'][$indexable_column_value];
if (!is_array($column)) {
$column = ['data' => []];
}
foreach (array_keys($fields) as $field) {
// Skip our column/row as we've already handled those.
if ($field == $options['column'] || $field == $options['row']) {
continue;
}
if (empty($fields[$field]->options['exclude'])) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$column['data'][$field][] = ['#markup' => $handler->getField($num, $field)];
}
}
// Add to our total, if required.
if (!empty($options['total_field'])) {
$variables['rows'][$indexable_row_value]['data']['_total'] += $fields[$options['total_field']]->getValue($row);
}
// If we have a percentage total, add that into the row.
if (isset($row->_percentage_total)) {
$variables['rows'][$indexable_row_value]['#percentage_total'] = $row->_percentage_total;
}
}
// Loop over our totals and render them as the single values get rendered.
if (!empty($options['total_field'])) {
$total_handler = $fields[$options['total_field']];
foreach ($variables['rows'] as &$row) {
// Add our total column.
$result_row = new ResultRow();
$result_row->{$total_handler->field_alias} = $row['data']['_total'];
$row['data']['_total'] = $fields[$options['total_field']]->render($result_row);
// Render our percentages, if requested.
if (!empty($options['percentage'])) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$total = $row['#percentage_total'] ?? str_replace([' ', ','], '', $row['data']['_total']);
if (empty($total)) {
continue;
}
foreach (array_keys($row['data']) as $field) {
// Skip our column/row as we've already handled those.
if ($field === $options['row'] || ($field === '_total' && !isset($row['#percentage_total']))) {
continue;
}
if (!is_array($row['data'][$field])) {
$value = $row['data'][$field];
$row['data'][$field] = [
$options['total_field'] => ['#markup' => $value],
];
}
else {
$value = $row['data'][$field][$options['total_field']]['#markup'];
}
$value = str_replace([' ', ','], '', strip_tags($value));
if (empty($value)) {
continue;
}
$row['data'][$field]['_percentage'] = [
'#type' => 'html_tag',
'#tag' => 'small',
'#value' => round($value / $total * 100, 1, PHP_ROUND_HALF_EVEN) . '%',
'#prefix' => ' ',
];
if (!empty($options['percentage_title'])) {
$row['data'][$field]['_percentage']['#attributes']['title'] = $options['percentage_title'];
}
}
}
}
}
// Add the caption to the list if set.
if (!empty($handler->options['caption'])) {
$variables['caption'] = ['#markup' => $handler->options['caption']];
$variables['caption_needed'] = TRUE;
}
elseif (!empty($variables['title'])) {
$variables['caption'] = ['#markup' => $variables['title']];
$variables['caption_needed'] = TRUE;
}
else {
$variables['caption'] = '';
$variables['caption_needed'] = FALSE;
}
$variables['summary'] = $handler->options['summary'];
$variables['description'] = $handler->options['description'];
$variables['caption_needed'] |= !empty($variables['summary']) || !empty($variables['description']);
}
/**
* Implements hook_form_alter().
*/
function contacts_form_alter(&$form, FormStateInterface $form_state, $form_id) {
Dashboard::fastHookFormAlter($form, $form_state, $form_id, 'contacts.dashboard');
}
/**
* Implements hook_form_FORM_ID_alter() for commerce_order_add_form.
*/
function contacts_form_commerce_order_add_form_alter(array &$form, FormStateInterface $form_state): void {
contacts_commerce_customer_form_alter($form, $form_state);
}
/**
* Implements hook_form_FORM_ID_alter() for commerce_order_reassign_form.
*/
function contacts_form_commerce_order_reassign_form_alter(array &$form, FormStateInterface $form_state): void {
contacts_commerce_customer_form_alter($form, $form_state);
}
/**
* Alter the commerce customer forms.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
function contacts_commerce_customer_form_alter(array &$form, FormStateInterface $form_state): void {
if (isset($form['customer']['uid']) && $form['customer']['uid']['#type'] === 'entity_autocomplete') {
$form['customer']['uid']['#placeholder'] = new TranslatableMarkup('Search by name or email address');
$form['customer']['uid']['#selection_handler'] = 'search_api';
$form['customer']['uid']['#selection_settings'] = [
'index' => 'contacts_index',
];
}
}
/**
* Implements hook_menu_local_actions_alter().
*/
function contacts_menu_local_actions_alter(&$local_actions) {
$config = \Drupal::config('contacts.configuration');
$contact_form_type = $config->get('add_contact_form_type') ?? 'legacy';
if ($contact_form_type === 'legacy') {
// If we're using the old/legacy 'Add Contact' forms then we need to add
// attributes to the menu items so they open in a modal dialog.
$local_actions['contacts.contact_add_org']['options']['attributes'] = [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-ajax-progress' => 'fullscreen',
];
$local_actions['contacts.contact_add_indiv']['options']['attributes'] = [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-ajax-progress' => 'fullscreen',
];
}
}
/**
* Implements hook_form_FORM_ID_alter() for entity_merge_verify_form.
*/
function contacts_form_entity_merge_verify_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// If entity_merge module is installed, add a validation handler to ensure
// an individual and an organisation can't be merged with each other.
$route_match = \Drupal::routeMatch();
if ($route_match->getParameter('entity_type') === 'user') {
$user_storage = \Drupal::entityTypeManager()->getStorage('user');
$primary_id = $route_match->getParameter('primary_id');
$secondary_id = $route_match->getParameter('secondary_id');
/** @var \Drupal\user\UserInterface $primary */
/** @var \Drupal\user\UserInterface $secondary */
$primary = $user_storage->load($primary_id);
$secondary = $user_storage->load($secondary_id);
if ($primary && $secondary) {
$primary_indiv_secondary_org = $primary->hasRole('crm_indiv') && $secondary->hasRole('crm_org');
$primary_org_secondary_indiv = $primary->hasRole('crm_org') && $secondary->hasRole('crm_indiv');
if ($primary_indiv_secondary_org || $primary_org_secondary_indiv) {
\Drupal::messenger()->addError(new TranslatableMarkup('Cannot merge an individual with an organisation.'));
$form['merge']['actions']['submit']['#access'] = FALSE;
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for search_api.
*/
function contacts_search_api_index_insert(EntityInterface $entity) {
// When our contacts index is inserted, we want to index users so they
// show up on the contacts dashboard. However, we don't want to necessarily
// do this for existing systems that may have many users, so only do it if
// the number of users in the system is less than the
// default entity batch size. We only want to do this for new installs of
// contacts, not from config sync.
/** @var \Drupal\search_api\Entity\Index $entity */
if ($entity->id() === 'contacts_index') {
try {
$batch_size = Settings::get('entity_update_batch_size', 50);
$count = $entity->getTrackerInstance()->getRemainingItemsCount();
if ($count < $batch_size && !\Drupal::isConfigSyncing()) {
$entity->indexItems();
}
}
catch (SearchApiException $ex) {
// If indexing fails silently give up and just log the error, we don't
// want to prevent the installation process.
$logger = \Drupal::logger('contacts');
Error::logException($logger, $ex);
}
}
}
/**
* Implements hook_module_implements_alter().
*/
function contacts_module_implements_alter(&$implementations, $hook) {
// If the password_policy module is installed, it hooks into the user form
// with password_policy_form_user_form_alter, however these changes are only
// compatible with the standard user form and not our custom AddContactsForm.
// Remove the password policy hook and ensure it's only conditionally invoked.
// See contacts_form_user_form_alter.
if ($hook === 'form_user_form_alter' && isset($implementations['password_policy'])) {
unset($implementations['password_policy']);
}
}
/**
* Implements hook_preprocess_HOOK() for input elements.
*/
function contacts_preprocess_input(&$variables) {
if (isset($variables['element']['#type']) && $variables['element']['#type'] == 'entity_autocomplete') {
$variables['#attached']['library'][] = 'contacts/multiline_autocomplete';
}
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter() for name_default.
*/
function contacts_field_widget_name_default_form_alter(&$element, FormStateInterface $form_state, $context) {
// Hide the crm_name field label.
// @todo Remove this if Name provides a way to do this natively.
// see https://www.drupal.org/project/name/issues/3325339.
if (isset($context['items']) && $context['items']->getName() === 'crm_name') {
$element['#title_display'] = 'none';
}
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter() for address_default.
*/
function contacts_field_widget_address_default_form_alter(&$element, FormStateInterface $form_state, $context) {
// Ensure that the address section is not collapsible.
// By default it renders as #type => 'details' which is collapsible.
if (isset($context['items']) && ($context['items']->getName() === 'crm_address' || $context['items']->getName() === 'crm_org_address')) {
$element['#type'] = 'container';
}
}
