SU-SWS/stanford_profile_helper

View on GitHub
stanford_profile_helper.module

Summary

Maintainability
Test Coverage
<?php

/**
 * @file
 * stanford_profile_helper_helper.module
 */

use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\search_api\IndexInterface;
use Drupal\stanford_profile_helper\StanfordProfileHelper;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy_menu\Plugin\Menu\TaxonomyMenuMenuLink;
use Drupal\user\RoleInterface;
use Drupal\views\ViewEntityInterface;

/**
 * Implements hook_module_implements_alter().
 */
function stanford_profile_helper_module_implements_alter(&$implementations, $hook) {
  if ($hook === 'plugin_filter_block__layout_builder_alter') {
    unset($implementations['menu_block']);
  }
}

/**
 * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
 *
 * Curate the blocks available in the Layout Builder "Add Block" UI.
 */
function stanford_profile_helper_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
  foreach ($definitions as &$definition) {
    if ($definition['provider'] == 'menu_block') {
      // Change the category for blocks provided by the menu block module so it
      // is separate from the "system" menus.
      $definition['category'] = t('Menu Block');
    }
  }
}


/**
 * Implements hook_cron().
 */
function stanford_profile_helper_cron() {
  if (!\Drupal::hasService('config_pages.loader')) {
    return;
  }
  /** @var \Drupal\config_pages\ConfigPagesLoaderServiceInterface $config_pages */
  $config_pages = \Drupal::service('config_pages.loader');

  $canonical_url = $config_pages->getValue('stanford_basic_site_settings', 'su_site_url', 0, 'value');
  $owners = $config_pages->getValue('stanford_basic_site_settings', 'su_site_owner_contact', [], 'value');
  $site_managers = $config_pages->getValue('stanford_basic_site_settings', 'su_site_tech_contact', [], 'value');
  $ally = $config_pages->getValue('stanford_basic_site_settings', 'su_site_a11y_contact', [], 'value');
  $org_ids = $config_pages->getValue('stanford_basic_site_settings', 'su_site_org', [], 'target_id');
  $renewal_date = $config_pages->getValue('stanford_basic_site_settings', 'su_site_renewal_due', 0, 'value');

  $orgs = \Drupal::entityTypeManager()
    ->getStorage('taxonomy_term')
    ->loadMultiple($org_ids);

  foreach ($orgs as &$org) {
    $org = $org->label();
  }

  $site_information = [
    'siteName' => \Drupal::config('system.site')->get('name'),
    'canonicalUrl' => $canonical_url,
    'owners' => $owners ?: [],
    'siteManagers' => $site_managers ?: [],
    'accessibility' => $ally ?: [],
    'organizations' => array_values($orgs) ?: [],
    'renewalDate' => $renewal_date,
  ];
  $uri = 'private://stanford';
  $directory = \Drupal::service('stream_wrapper_manager')->normalizeUri($uri);
  $file_system = \Drupal::service('file_system');
  $file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
  $file_system->saveData(json_encode($site_information, JSON_PRETTY_PRINT), "$directory/site-info.json", FileSystemInterface::EXISTS_REPLACE);
}

/**
 * Implements hook_cache_flush().
 */
function stanford_profile_helper_cache_flush() {
  $queries = [];
  /** @var \Drupal\views\Entity\View[] $views */
  $views = \Drupal::entityTypeManager()->getStorage('view')
    ->loadByProperties(['status' => TRUE]);
  foreach ($views as $view) {
    foreach ($view->get('display') as $display) {
      $filters = $display['display_options']['filters'] ?? [];
      foreach ($filters as $filter) {
        $queries[] = $filter['expose']['identifier'] ?? NULL;
      }
    }
  }
  $queries = array_unique(array_filter($queries));
  asort($queries);
  \Drupal::state()
    ->set('page_cache_query_ignore.view_params', array_values($queries));
}

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function stanford_profile_helper_view_update(ViewEntityInterface $view) {
  // After updating a view, reset the page_cache_query_ignore state.
  stanford_profile_helper_cache_flush();
}

/**
 * Implements hook_entity_type_alter().
 */
function stanford_profile_helper_entity_type_alter(array &$entity_types) {
  if (isset($entity_types['menu_link_content'])) {
    $entity_types['menu_link_content']->addConstraint('menu_link_item_url_constraint');
  }
}

/**
 * Implements hook_local_tasks_alter().
 */
function stanford_profile_helper_local_tasks_alter(&$local_tasks) {
  // Remove this when a new release for the scheduler module comes out above
  // 2.0.0-alpha1.
  // @see https://www.drupal.org/project/scheduler/issues/3224340.
  unset($local_tasks['scheduler.scheduled_content'], $local_tasks['scheduler.scheduled_media'], $local_tasks['scheduler.media_overview']);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function stanford_profile_helper_form_taxonomy_overview_terms_alter(&$form, FormStateInterface $form_state) {
  if ($form_state->get('taxonomy')['vocabulary']->id() == 'stanford_publication_topics') {
    $url = Url::fromUri('https://userguide.sites.stanford.edu/tour/publications#publications-list-page');
    $link = Link::fromTextAndUrl(t('default Publications List Page'), $url)
      ->toString();
    $form['citation_format']['#title'] = t('Citation Style');
    $form['citation_format']['#description'] = t('Select citation format for the %link. *<strong>CAUTION</strong>: The default Publication list page uses Chicago as the citation style. If you select a different citation format here, you should also update the citation format on the default Publications List Page that uses a "filter by topics" menu.', ['%link' => $link]);
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function stanford_profile_helper_redirect_presave(EntityInterface $redirect) {
  // Purge everything for the source url so that it can redirect without any
  // intervention.
  if (\Drupal::moduleHandler()->moduleExists('purge_processor_lateruntime')) {
    $source = $redirect->get('redirect_source')->getString();
    _stanford_profile_helper_purge_path($source);
  }
}

/**
 * Purges a relative path using the generated absolute url.
 *
 * @param string $path
 *   Drupal site relative path.
 *
 * @throws \Drupal\purge\Plugin\Purge\Invalidation\Exception\InvalidExpressionException
 * @throws \Drupal\purge\Plugin\Purge\Invalidation\Exception\MissingExpressionException
 * @throws \Drupal\purge\Plugin\Purge\Invalidation\Exception\TypeUnsupportedException
 */
function _stanford_profile_helper_purge_path($path) {
  $url = Url::fromUserInput('/' . trim($path, '/'), ['absolute' => TRUE])
    ->toString(TRUE)->getGeneratedUrl();

  $purgeInvalidationFactory = \Drupal::service('purge.invalidation.factory');
  $purgeProcessors = \Drupal::service('purge.processors');
  $purgePurgers = \Drupal::service('purge.purgers');

  $processor = $purgeProcessors->get('lateruntime');
  $invalidations = [$purgeInvalidationFactory->get('url', $url)];

  try {
    $purgePurgers->invalidate($processor, $invalidations);
  }
  catch (\Exception $e) {
    \Drupal::logger('stanford_profile_helper')->error($e->getMessage());
  }
}

/**
 * Implements hook_page_attachments().
 */
function stanford_profile_helper_page_attachments(array &$attachments) {
  $env = getenv('AH_SITE_ENVIRONMENT');
  // Add SiteImprove analytics for anonymous users on prod sites.
  // ACE prod is 'prod'; ACSF can be '01live', '02live', ...
  if (
    \Drupal::currentUser()->isAnonymous() &&
    ($env === 'prod' || preg_match('/^\d*live$/', $env))
  ) {
    $attachments['#attached']['library'][] = 'stanford_profile_helper/siteimprove.analytics';
  }
}

/**
 * Implements hook_filter_info_alter().
 */
function stanford_profile_helper_filter_info_alter(&$info) {
  if (
    isset($info['filter_mathjax']) &&
    \Drupal::moduleHandler()->moduleExists('mathjax')
  ) {
    $info['filter_mathjax']['class'] = 'Drupal\stanford_profile_helper\Plugin\Filter\Mathjax';
  }
}

/**
 * Implements hook_theme().
 */
function stanford_profile_helper_theme($existing, $type, $theme, $path) {
  $themes['block__stanford_basic_search'] = [
    'template' => 'block--stanford-basic-search',
    'original hook' => 'block',
  ];
  $themes['rabbit_hole_message'] = [
    'variables' => ['destination' => NULL],
  ];
  return $themes;
}

/**
 * Implements hook_library_info_alter().
 */
function stanford_profile_helper_library_info_alter(&$libraries, $extension) {
  if ($extension == 'views') {
    $libraries['views.ajax']['dependencies'][] = 'stanford_profile_helper/ajax_views';
  }

  if ($extension == 'mathjax') {
    $libraries['source']['dependencies'][] = 'stanford_profile_helper/mathjax';
    unset($libraries['setup'], $libraries['config']);
  }

  // Rely on the fontawesome module to provide the library.
  if (
    $extension == 'stanford_basic' &&
    \Drupal::moduleHandler()->moduleExists('fontawesome')
  ) {
    unset($libraries['fontawesome']);
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function stanford_profile_helper_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  unset($form['actions']['unlock'], $form['scheduler_settings']);
  $node = $form_state->getBuildInfo()['callback_object']->getEntity();
  $system_pages = \Drupal::config('system.site')->get('page');
  $access = !in_array('/node/' . $node->id(), $system_pages);

  $scheduler_increment = \Drupal::state()
    ->get('stanford_profile_helper.scheduler_increment', 60 * 60 * 4);
  $hours = (int) floor($scheduler_increment / 3600);
  $mins = (int) floor($scheduler_increment / 60 % 60);
  $scheduler_increment = $hours ? "$hours hour(s)" : "$mins minute(s)";
  $example_start = new DateTime('today 8:00 AM');
  $example_end = clone $example_start;
  if ($hours > 0) {
    $example_end->modify("+$hours hours");
  }
  $example_end->modify("+$mins minutes");

  $help_text = [
    t('Select a date and time* to publish this content in the future.'),
    t('After scheduling the publish, it will automatically publish to your site on the scheduled date within @times of the selected time.', [
      '@times' => $scheduler_increment,
    ]),
    t('For example, if you select @start as the publish time, the content will be published between @start and @end.', [
      '@start' => $example_start->format('H:i'),
      '@end' => $example_end->format('H:i'),
    ]),
    t('<p><strong>*Note</strong>: You must select a time that is increments of @times, starting with 12AM.</p>', [
      '@times' => $scheduler_increment,
    ]),
  ];

  $form['scheduling'] = [
    '#type' => 'container',
    '#group' => 'revision_information',
    '#access' => (isset($form['unpublish_on']) || isset($form['unpublish_on'])) && !in_array('/node/' . $node->id(), $system_pages),
    'help' => ['#markup' => implode(' ', $help_text)],
    '#weight' => 999,
  ];

  if (isset($form['unpublish_on'])) {
    $form['unpublish_on']['#group'] = 'revision_information';
    $form['unpublish_on']['#weight'] = 55;
    $form['scheduling']['unpublish_on'] = $form['unpublish_on'];
  }

  if (isset($form['publish_on'])) {
    $form['publish_on']['#group'] = 'revision_information';
    $form['publish_on']['#weight'] = 50;
    $form['unpublish_on']['#access'] = $access;
    $status_element = &$form['status']['widget']['value'];
    $status_element['#states'] = [
      'disabled' => [':input[name="publish_on[0][value][time]"]' => ['filled' => TRUE]],
    ];
    $form['scheduling']['publish_on'] = $form['publish_on'];
  }
  unset($form['publish_on'], $form['unpublish_on']);
}

/**
 * Implements hook_form_alter().
 */
function stanford_profile_helper_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (strpos($form_id, 'views_form_') === 0) {
    // Remove the select all since it selects every node, not just the ones
    // from the active filters.
    // @link https://www.drupal.org/project/views_bulk_operations/issues/3055770#comment-13116724
    unset($form['header']['views_bulk_operations_bulk_form']['select_all']);

    // Sort the action menu options alphabetically.
    if (!empty($form['header']['views_bulk_operations_bulk_form']['action']['#options'])) {
      $actions_array = $form['header']['views_bulk_operations_bulk_form']['action']['#options'];
      uasort($actions_array, function ($a, $b) {
        return strcasecmp((string) $a, (string) $b);
      });
      $form['header']['views_bulk_operations_bulk_form']['action']['#options'] = $actions_array;
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function stanford_profile_helper_form_views_bulk_operations_configure_action_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (!empty($form['node']['stanford_event']['su_event_date_time']['widget'])) {
    $form['node']['stanford_event']['su_event_date_time']['widget'][0]['time_wrapper']['value']['#required'] = FALSE;
    $form['node']['stanford_event']['su_event_date_time']['widget'][0]['time_wrapper']['end_value']['#required'] = FALSE;
  }
}

/**
 * Implements hook_preprocess_ds_entity_view().
 */
function stanford_profile_helper_preprocess_ds_entity_view(&$variables) {
  $variables['content']['#pre_render'][] = [
    'Drupal\stanford_profile_helper\StanfordProfileHelper',
    'preRenderDsEntity',
  ];
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function stanford_profile_helper_form_taxonomy_term_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
  $vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
  $flat_taxonomy = $vocabulary->getThirdPartySetting('flat_taxonomy', 'flat');

  // Tweak the taxonomy term add/edit form.
  if (!empty($form['relations']['parent']) && !$flat_taxonomy) {
    $form['relations']['#open'] = TRUE;
    $form['relations']['parent']['#multiple'] = FALSE;
    $form['relations']['parent']['#title'] = t('Parent term');
    $form['relations']['parent']['#description'] = t('Select the appropriate parent item for this term.');
    $form['relations']['parent']['#element_validate'][] = '_stanford_profile_helper_term_form_validate';
  }
}

/**
 * Tweak the taxonomy term parent form value after submitting.
 *
 * Because we are changing the form to not allow multiple parents, the form
 * value needs to be changed into an array so the TermForm can still manage
 * it correctly.
 *
 * @param array $element
 *   Form element.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Current form state object.
 * @param array $form
 *   Complete form.
 *
 * @see stanford_profile_helper_form_taxonomy_term_form_alter()
 */
function _stanford_profile_helper_term_form_validate(array $element, FormStateInterface $form_state, array $form) {
  $form_state->setValueForElement($element, [$element['#value']]);
}

/**
 * Implements hook_menu_links_discovered_alter().
 */
function stanford_profile_helper_menu_links_discovered_alter(&$links) {
  if (isset($links['admin_toolbar_tools.extra_links:media_page'])) {
    // Alter the "Media" link for /admin/content/media path.
    $links['admin_toolbar_tools.extra_links:media_page']['title'] = t('All Media');
  }
  if (isset($links['system.admin_content'])) {
    // Change the node list page for the /admin/content path.
    $links['system.admin_content']['title'] = t('All Content');
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function stanford_profile_helper_preprocess_block__help(&$variables) {
  if (\Drupal::routeMatch()->getRouteName() == 'help.main') {
    // Removes the help text from core help module. Its not helpful, and we're
    // going to provide our own help text.
    // @see help_help()
    unset($variables['content']);
  }
}

/**
 * Implements hook_help_section_info_alter().
 */
function stanford_profile_helper_help_section_info_alter(array &$info) {
  // Change "Module overviews" header.
  $info['hook_help']['title'] = t('For Developers');
}

/**
 * Implements hook_entity_field_access().
 */
function stanford_profile_helper_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  if (
    $operation != 'view' &&
    $field_definition->getName() == 'status' &&
    $field_definition->getTargetEntityTypeId() == 'node' &&
    $items &&
    $items->getEntity()->id()
  ) {
    // Prevent unpublishing the home, 404 and 403 pages.
    return stanford_profile_helper_node_access($items->getEntity(), 'delete', $account);
  }

  if (
    $field_definition->getType() == 'entity_reference' &&
    $field_definition->getSetting('handler') == 'layout_library' &&
    $operation == 'edit'
  ) {
    $entity_type = $field_definition->getTargetEntityTypeId();
    $bundle = $field_definition->getTargetBundle();
    if (!$account->hasPermission("choose layout for $entity_type $bundle")) {
      return AccessResult::forbidden();
    }
  }
  $route_match = \Drupal::routeMatch();
  // When the page title banner is in use on the page, disable the node title
  // field access because the field title will be used in the banner.
  if (
    $operation == 'view' &&
    $field_definition->getName() == 'title' &&
    $items?->getEntity()->getEntityTypeId() == 'node' &&
    $items->getEntity()->bundle() == 'stanford_page' &&
    $items->getEntity()->get('su_page_banner')->count() &&
    $route_match->getRouteName() == 'entity.node.canonical' &&
    $route_match->getParameter('node')->id() == $items->getEntity()->id()
  ) {
    // Now we know we are on the node page and the node has a banner paragraph
    // of some sort. If the banner paragraph is the correct type, we can prevent
    // the original node title from displaying.
    /** @var \Drupal\paragraphs\ParagraphInterface $banner_paragraph */
    $banner_paragraph = $items->getEntity()->get('su_page_banner')->get(0)->entity;
    return AccessResult::forbiddenIf($banner_paragraph->bundle() == 'stanford_page_title_banner');
  }

  return AccessResult::neutral();
}

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function stanford_profile_helper_field_widget_options_select_form_alter(&$element, FormStateInterface $form_state, $context) {
  if ($context['items']->getFieldDefinition()
      ->getName() == 'layout_selection') {
    $element['#description'] = t('Choose a layout to display the page as a whole. Choose "- None -" to keep the default layout setting.');
  }
}

/**
 * Implements hook_preprocess_toolbar().
 */
function stanford_profile_helper_preprocess_toolbar(&$variables) {
  array_walk($variables['tabs'], function (&$tab, $key) {
    if (isset($tab['attributes'])) {
      $tab['attributes']->addClass(Html::cleanCssIdentifier("$key-tab"));
    }
  });
}

/**
 * Implements hook_contextual_links_alter().
 */
function stanford_profile_helper_contextual_links_alter(array &$links, $group, array $route_parameters) {
  if ($group == 'paragraph') {
    // Paragraphs edit module clone link does not function correctly. Remove it
    // from available links. Also remove delete to avoid unwanted delete.
    unset($links['paragraphs_edit.delete_form']);
    unset($links['paragraphs_edit.clone_form']);
  }
}

/**
 * Implements hook_node_access().
 */
function stanford_profile_helper_node_access(NodeInterface $node, $op, AccountInterface $account) {
  if ($op == 'delete') {
    $site_config = \Drupal::config('system.site');
    $node_urls = [$node->toUrl()->toString(TRUE)->getGeneratedUrl(), "/node/{$node->id()}"];

    // If the node is configured to be the home page, 404, or 403, prevent the
    // user from deleting. Unfortunately this only works for roles without the
    // "Bypass content access control" permission.
    if (array_intersect($node_urls, $site_config->get('page'))) {
      return AccessResult::forbidden();
    }
  }
  return AccessResult::neutral();
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function stanford_profile_helper_form_menu_edit_form_alter(array &$form, FormStateInterface $form_state) {
  $read_only = Settings::get('config_readonly', FALSE);
  if (!$read_only) {
    return;
  }

  // If the form is locked, hide the config you cannot change from users without
  // the know how.
  $access = \Drupal::currentUser()
    ->hasPermission('Administer menus and menu items');
  $form['label']['#access'] = $access;
  $form['description']['#access'] = $access;
  $form['id']['#access'] = $access;

  // Remove the warning message if the user does not have access.
  if (!$access) {
    \Drupal::messenger()->deleteByType("warning");
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function stanford_profile_helper_form_config_pages_stanford_basic_site_settings_form_alter(array &$form, FormStateInterface $form_state) {
  $form['#validate'][] = 'stanford_profile_helper_config_pages_stanford_basic_site_settings_form_validate';
}

/**
 * Validates form values.
 *
 * @param array $form
 *   The form array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state interface object.
 */
function stanford_profile_helper_config_pages_stanford_basic_site_settings_form_validate(array $form, FormStateInterface $form_state) {
  $element = $form_state->getValue('su_site_url');
  $uri = $element['0']['uri'];
  if (!empty($uri)) {
    // Test if the site url submmitted is equal to current domain.
    $host = \Drupal::request()->getSchemeAndHttpHost();
    if ($host != $uri) {
      $form_state->setErrorByName('su_site_url', t('This URL does not match your domain.'));
    }
  }
}

/**
 * Alter the data of a sitemap link before the link is saved.
 *
 * @param array $link
 *   An array with the data of the sitemap link.
 * @param array $context
 *   An optional context array containing data related to the link.
 */
function stanford_profile_helper_xmlsitemap_link_alter(array &$link, array $context) {

  // Get node/[:id] from loc.
  $node_id = $link['loc'];

  // Get 403 page path.
  $stanford_profile_helper_403_page = \Drupal::config('system.site')
    ->get('page.403');

  // Get 404 page path.
  $stanford_profile_helper_404_page = \Drupal::config('system.site')
    ->get('page.404');

  // If node id matches 403 or 404 pages, remove it from sitemap.
  switch ($node_id) {
    case $stanford_profile_helper_403_page:
    case $stanford_profile_helper_404_page:
      // Status is set to zero to exclude the item in the sitemap.
      $link['status'] = 0;

  }
}

/**
 * Implements hook_config_readonly_whitelist_patterns().
 */
function stanford_profile_helper_config_readonly_whitelist_patterns() {
  $default_theme = \Drupal::config('system.theme')->get('default');
  // Allow the theme settings to be changed in the UI.
  $patterns = ["$default_theme.settings", 'next.next_site.*'];

  // Allow the form to be submitted in the UI for specific routes that don't
  // alter the configuration, such as resetting the order of taxonomy terms.
  $routes_to_config = [
    'entity.taxonomy_vocabulary.reset_form' => ['taxonomy.vocabulary.*'],
    'entity.search_api_index.rebuild_tracker' => ['search_api.index.*'],
    'entity.search_api_index.clear' => ['search_api.index.*'],
    'entity.search_api_index.reindex' => ['search_api.index.*'],
    'xmlsitemap.admin_rebuild' => ['xmlsitemap.settings'],
  ];

  $route_name = \Drupal::routeMatch()->getRouteName();
  if (isset($routes_to_config[$route_name])) {
    $patterns = [...$patterns, ...$routes_to_config[$route_name]];
  }
  return $patterns;
}

/**
 * Implements field_group_form_process_build_alter().
 */
function stanford_profile_helper_field_group_form_process_build_alter(&$element) {
  // Hide / Show the field groups based on the enabled checkbox.
  if (isset($element['group_lockup_options'])) {
    $element['group_lockup_options']['#states'] = [
      'visible' => [
        ':input[name="su_lockup_enabled[value]"]' => [
          'checked' => FALSE,
        ],
      ],
    ];
    $element['group_logo_image']['#states'] = [
      'visible' => [
        ':input[name="su_lockup_enabled[value]"]' => [
          'checked' => FALSE,
        ],
      ],
    ];
  }
}

/**
 * Creates a states array.
 *
 * @param array $opts
 *   Allowed values.
 * @param string $input
 *   Field selector.
 *
 * @return array
 *   State array.
 */
function _stanford_profile_helper_get_lockup_states(array $opts, $input) {
  $ret = [];
  foreach ($opts as $val) {
    $ret[] = [
      $input => ['value' => $val],
    ];
  }
  return $ret;
}

/**
 * Implements hook_entity_type_update().
 */
function stanford_profile_helper_taxonomy_term_update(TermInterface $entity) {
  // https://www.drupal.org/project/taxonomy_menu/issues/2867626
  $original_parent = $entity->original->get('parent')->getString();
  if ($original_parent == $entity->get('parent')->getString()) {
    return;
  }
  $database = \Drupal::database();
  $menu_link_exists = $database->select('menu_tree', 'm')->fields('m')
    ->condition('id', 'taxonomy_menu.menu_link%', 'LIKE')
    ->condition('route_param_key', 'taxonomy_term=' . $entity->id())
    ->countQuery()
    ->execute()
    ->fetchField();

  if ($menu_link_exists > 0) {
    $database->delete('menu_tree')
      ->condition('id', 'taxonomy_menu.menu_link%', 'LIKE')
      ->condition('route_param_key', 'taxonomy_term=' . $entity->id())
      ->execute();
    \Drupal::service('router.builder')->rebuild();
  }
}

/**
 * Implements hook_preprocess_pattern_NAME().
 */
function stanford_profile_helper_preprocess_pattern_alert(&$variables) {
  $entity_type = $variables['context']->getProperty('entity_type');
  $bundle = $variables['context']->getProperty('bundle');
  $entity = $variables['context']->getProperty('entity');

  // Global Messages!
  if ($entity_type == "config_pages" && $bundle == "stanford_global_message") {

    // Validate that the entity has the field we need so we don't 500 the site.
    if (!$entity->hasField('su_global_msg_type')) {
      \Drupal::logger('stanford_profile_helper')
        ->error("Global Messages Config Block is missing the field su_global_msg_type");
      return;
    }

    $color = $entity->get('su_global_msg_type')->getString();
    $variables['attributes']->addClass("su-alert--" . $color);
    $dark_bgs = ['error', 'info', 'success'];
    if (in_array($color, $dark_bgs)) {
      $variables['attributes']->addClass("su-alert--text-light");
    }
  }

}

/**
 * Implements hook_preprocess_pattern_NAME().
 */
function stanford_profile_helper_preprocess_pattern_localfooter(&$variables) {
  $entity_type = $variables['context']->getProperty('entity_type');
  $bundle = $variables['context']->getProperty('bundle');
  $entity = $variables['context']->getProperty('entity');

  // If the pattern has already been rendered and the 2nd cell is already a
  // markup object or other, we can't manipulate it.
  if (!is_array($variables['cell2'])) {
    return;
  }
  $second_content = $variables['cell2']['su_local_foot_se_co'] ?? [];
  $third_content = $variables['cell2']['su_local_foot_tr2_co'] ?? [];

  // The local footer pattern from Decanter doesn't have equal columns for each
  // of the 3 content blocks. To avoid having to completely rewrite the
  // template, we can use some special markup to wrap a couple fields with the
  // necessary classes to simulate the 4 equal columns. If the user populates
  // 1, 2, and 4 content blocks, then the 2nd block will stretch to fill take
  // the area of the missing 3rd block.
  if (\Drupal::service('renderer')->render($third_content)) {
    $variables['cell2'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['flex-container']],
      'second' => [
        '#type' => 'container',
        '#attributes' => ['class' => ['flex-md-6-of-12', 'su-margin-bottom-1']],
        'contents' => $second_content,
      ],
      'third' => [
        '#type' => 'container',
        '#attributes' => ['class' => ['flex-md-6-of-12']],
        'contents' => $variables['cell2']['su_local_foot_tr2_co'],
      ],
    ];
  }

  // Local Footer!
  if ($entity_type == "config_pages" && $bundle == "stanford_local_footer") {

    // If the lockup updates are not enabled just end.
    if (
      !$entity->hasField('su_local_foot_use_loc')
      || $entity->get('su_local_foot_use_loc')->getString() === "1"
    ) {
      return;
    }

    // Enable custom lockup.
    $variables['custom_lockup'] = TRUE;

    // Lockup customizations are enabled.
    $variables['line1'] = $entity->get('su_local_foot_line_1')->getString();
    $variables['line2'] = $entity->get('su_local_foot_line_2')->getString();
    $variables['line3'] = $entity->get('su_local_foot_line_3')->getString();
    $variables['line4'] = $entity->get('su_local_foot_line_4')->getString();
    $variables['line5'] = $entity->get('su_local_foot_line_5')->getString();
    $variables['use_logo'] = $entity->get('su_local_foot_use_logo')
      ->getString();
    $file_field = $entity->get('su_local_foot_loc_img');

    // Check if there is a file.
    if (isset($file_field->entity)) {
      $file_uri = $file_field->entity->getFileUri();
      $variables['site_logo'] = \Drupal::service('file_url_generator')->generateAbsoluteString($file_uri);
    }
    else {
      $variables['use_logo'] = "1";
    }

    // Check if there is a link and patch it through.
    $link = $entity->get('su_local_foot_loc_link')->getString();
    if ($link) {
      $variables['lockup_link'] = [
        '#markup' => URL::fromUri($link)->toString(TRUE)->getGeneratedUrl(),
      ];
    }

    // Pass through the lockup option.
    $option = $entity->get('su_local_foot_loc_op')->getString();
    $variables['lockup_option'] = 'su-lockup--option-' . $option;
  }

}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function stanford_profile_helper_form_media_library_add_form_embeddable_alter(array &$form, FormStateInterface $form_state) {

  $source_field = $form_state->get('source_field');
  $embed_code_field = $form_state->get('unstructured_field_name');
  $user = \Drupal::currentUser();
  $authorized = $user->hasPermission('create field_media_embeddable_code')
    || $user->hasPermission('edit field_media_embeddable_code');

  if (isset($form['container'][$embed_code_field])) {
    $form['container'][$embed_code_field]['#access'] = $authorized;
  }

  if (isset($form['container'][$source_field])) {
    if (!$authorized) {
      $new_desc = 'Allowed providers: @providers. For custom embeds, please <a href="@snow_form">request support.</a>';
      $args = $form['container'][$source_field]['#description']->getArguments();
      $args['@snow_form'] = 'https://stanford.service-now.com/it_services?id=sc_cat_item&sys_id=83daed294f4143009a9a97411310c70a';
      $form['container'][$source_field]['#description'] = t($new_desc, $args);
    }
    $form['container'][$source_field]['#title'] = t('oEmbed URL');
  }

}

/**
 * Check the access for certain admin menu items and remove them if needed.
 *
 * @param array $menu_items
 *   Keyed array of menu item from preprocess_menu.
 */
function stanford_profile_helper_check_admin_menu_access(array &$menu_items): void {
  $current_user = \Drupal::currentUser();
  foreach ($menu_items as $key => &$item) {
    /** @var \Drupal\Core\Url $url */
    $url = $item['url'];

    $vid = $url->getRouteParameters()['taxonomy_vocabulary'] ?? FALSE;
    if (
      $vid &&
      !$current_user->hasPermission('administer taxonomy') &&
      !$current_user->hasPermission("create terms in $vid") &&
      !$current_user->hasPermission("delete terms in $vid") &&
      !$current_user->hasPermission("edit terms in $vid")
    ) {
      unset($menu_items[$key]);
      continue;
    }

    stanford_profile_helper_check_admin_menu_access($item['below']);
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function stanford_profile_helper_preprocess_menu(&$variables) {
  if ($variables['menu_name'] == 'admin') {
    stanford_profile_helper_check_admin_menu_access($variables['items']);
  }

  $cache_tags = $variables['#cache']['tags'] ?? [];
  foreach ($variables['items'] as &$item) {
    // Taxonomy menu link items use the description from the term as the title
    // attribute. The description can be very long and could contain HTML. To
    // Make things easiest, just remove the title attribute.
    if ($item['original_link'] instanceof TaxonomyMenuMenuLink) {
      $attributes = $item['url']->getOption('attributes');
      unset($attributes['title']);
      $item['url']->setOption('attributes', $attributes);

      $term = \Drupal::entityTypeManager()
        ->getStorage('taxonomy_term')
        ->load($item['url']->getRouteParameters()['taxonomy_term']);

      if ($term) {
        $cache_tags[] = 'taxonomy_term_list:' . $term->bundle();
        $cache_tags = array_merge($cache_tags, $term->getCacheTags());
      }
    }
  }
  $variables['#cache']['tags'] = array_unique($cache_tags);
}

/**
 * Implements hook_field_widget_form_alter().
 */
function stanford_profile_helper_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
  if ($context['items']->getName() == 'su_page_components') {
    // Push pages to only allow 3 items per row but don't break any existing
    // pages that have 4 per row.
    $element['container']['value']['#attached']['drupalSettings']['reactParagraphs'][0]['itemsPerRow'] = 3;
  }

  if ($context['items']->getName() == 'field_media_embeddable_oembed') {
    $user = \Drupal::currentUser();
    $authorized = $user->hasPermission('create field_media_embeddable_code')
      || $user->hasPermission('edit field_media_embeddable_code');
    if (!$authorized) {
      $args = $element['value']['#description']['#items'][1]->getArguments();
      $args['@snow_form'] = 'https://stanford.service-now.com/it_services?id=sc_cat_item&sys_id=83daed294f4143009a9a97411310c70a';
      $new_desc = 'Allowed providers: @providers. For custom embeds, please <a href="@snow_form">request support.</a>';
      $element['value']['#description'] = t($new_desc, $args);
    }
  }
}

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function stanford_profile_helper_field_widget_datetime_timestamp_no_default_form_alter(&$element, FormStateInterface $form_state, $context) {
  // Set the date increment for scheduler settings.
  $state = \Drupal::state();
  $element['value']['#date_increment'] = $state->get('stanford_profile_helper.scheduler_increment', 60 * 60 * 4);
}

/**
 * Implements hook_entity_bundle_field_info_alter().
 */
function stanford_profile_helper_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {

  if (
    $bundle == 'stanford_global_message' &&
    !empty($fields['su_global_msg_enabled'])
  ) {
    $fields['su_global_msg_enabled']->addConstraint('global_message_constraint', []);
  }

}

/**
 * Get available roles, limited if the role_delegation module is enabled.
 *
 * @return array
 *   Keyed array of role id and role label.
 */
function _stanford_profile_helper_get_assignable_roles(): array {
  if (\Drupal::moduleHandler()->moduleExists('role_delegation')) {
    /** @var \Drupal\role_delegation\DelegatableRolesInterface $role_delegation */
    $role_delegation = \Drupal::service('delegatable_roles');
    return $role_delegation->getAssignableRoles(\Drupal::currentUser());
  }

  $roles = \Drupal::entityTypeManager()
    ->getStorage('user_role')
    ->loadMultiple();
  unset($roles[RoleInterface::ANONYMOUS_ID]);
  return array_map(fn($role) => $role->label(), $roles);
}

/**
 * Implements hook_component_info_alter().
 */
function stanford_profile_helper_component_info_alter(&$components) {
  foreach ($components as $id => $component) {
    // Check if the provider of the PDB component is enabled.
    if (!_stanford_profile_helper_extension_enabled($component)) {
      unset($components[$id]);
    }
  }
}

/**
 * Traverse the PDB extension to see if it's module/theme/profile is enabled.
 *
 * @param \Drupal\Core\Extension\Extension $extension
 *   Discovered PDB extension object.
 *
 * @return bool
 *   If the PDB extension's provider is enabled.
 */
function _stanford_profile_helper_extension_enabled(Extension $extension) {
  $path = $extension->getPath();

  // Traverse down the path of the extension to find a module/theme/profile
  // that can be checked for existance.
  while ($path) {

    // An info.yml file exists in the current path, check if it's enabled as
    // a theme, profile, or module.
    if ($info_files = glob("$path/*.info.yml")) {
      $info_file_path = $info_files[0];
      $name = basename($info_file_path, '.info.yml');
      $info_file = Yaml::decode(file_get_contents($info_file_path));

      if (isset($info_file['type'])) {
        switch ($info_file['type']) {
          case 'theme':
            return \Drupal::service('theme_handler')->themeExists($name);

          case 'module':
          case 'profile':
            return \Drupal::moduleHandler()->moduleExists($name);
        }
      }
    }

    // Pop off the last part of the path to go one level higher.
    $path = explode('/', $path);
    array_pop($path);
    $path = implode('/', $path);
  }
  return FALSE;
}

/**
 * Implements hook_block_build_alter().
 */
function stanford_profile_helper_block_build_alter(array &$build, BlockPluginInterface $block) {
  if ($block->getBaseId() == 'system_menu_block') {
    $build['#cache']['tags'][] = 'stanford_profile_helper:menu_links';
    StanfordProfileHelper::removeCacheTags($build, [
      '^node:*',
      '^config:system.menu.*',
    ]);
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function stanford_profile_helper_preprocess_block__system_main_block(&$variables) {
  $variables['content']['#cache']['tags'][] = 'stanford_profile_helper:menu_links';
  // Remove node cache tags since we'll use our own cache tag above.
  StanfordProfileHelper::removeCacheTags($variables['content'], ['^config:system.menu.*']);
}

/**
 * Implements hook_preprocess_HOOK().
 */
function stanford_profile_helper_preprocess_block__system_menu_block(&$variables) {
  $variables['content']['#cache']['tags'][] = 'stanford_profile_helper:menu_links';
  // Remove node cache tags since we'll use our own cache tag above.
  StanfordProfileHelper::removeCacheTags($variables['content'], [
    '^node:*',
    '^config:system.menu.*',
  ]);
}

/**
 * Implements hook_entity_view().
 *
 * Modifies entity render arrays for fields that are of a certain type. The DS
 * module provides a limit option, but it doesn't work for all field formatters
 * because of the way the render array is structured. We'll move around the
 * field items, call the DS module function, and then correct the render array
 * back so that it still functions as expected.
 */
function stanford_profile_helper_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  $list_types = [
    'entity_reference_list_label_class',
    'link_list_class',
    'list_string_list_class',
    'string_list_class',
  ];
  $components = $display->getComponents();
  $list_render_arrays = [];
  // First, move the items from the list render array into the build of the
  // field. Store the list render array for re-use later.
  foreach ($components as $field => $component) {
    if (
      isset($component['type']) &&
      in_array($component['type'], $list_types) &&
      !empty($component['third_party_settings']['ds']['ds_limit']) &&
      !empty($build[$field][0]['#items'])
    ) {
      $list_render_arrays[$field] = $build[$field][0];
      // Pull out the items to be placed higher in the build array.
      $items = $list_render_arrays[$field]['#items'];
      unset($list_render_arrays[$field]['#items']);
      foreach ($items as $delta => $item) {
        $build[$field][$delta] = $item;
      }
    }
  }
  // We must have modified some stuff, so call the DS module function.
  if ($list_render_arrays) {
    ds_entity_view_alter($build, $entity, $display, $view_mode);
  }

  foreach ($list_render_arrays as $field => $render_array) {
    $deltas = Element::children($build[$field]);
    $items = [];
    // Pull the field items back out that have been limited by DS module and
    // put those back into the render array from earlier.
    foreach ($deltas as $delta) {
      $items[$delta] = $build[$field][$delta];
      unset($build[$field][$delta]);
    }
    $render_array['#items'] = $items;
    $build[$field][0] = $render_array;
  }
}

/**
 * Implements hook_search_api_processor_info_alter().
 */
function stanford_profile_helper_search_api_processor_info_alter(array &$processors) {
  $processors['custom_value']['class'] = '\Drupal\stanford_profile_helper\Plugin\search_api\processor\CustomValue';
}

/**
 * Implements hook_entity_view_display_alter().
 */
function stanford_profile_helper_entity_view_display_alter(EntityViewDisplayInterface $display, array $context) {
  if (str_contains($context['view_mode'], 'search_indexing') && $context['entity_type'] == 'node') {
    // The title is already in the template, it's not needed in the display.
    $display->removeComponent('title');
  }
}

/**
 * Implements hook_ENTITY_TYPE_access().
 */
function stanford_profile_helper_filter_format_access(EntityInterface $entity, $operation, AccountInterface $account) {
  return AccessResult::forbiddenIf($entity->id() == 'administrative_html');
}

/**
 * Implements hook_search_api_algolia_objects_alter().
 */
function stanford_profile_helper_search_api_algolia_objects_alter(array &$objects, IndexInterface $index, array $items) {
  /** @var \Drupal\config_pages\ConfigPagesLoaderServiceInterface $config_page_loader */
  $config_page_loader = \Drupal::service('config_pages.loader');

  // If the canonical url is set, use that to adjust the urls.
  $site_domain = $config_page_loader->getValue('stanford_basic_site_settings', 'su_site_url', 0, 'uri');
  $current_host = \Drupal::request()->getSchemeAndHttpHost();

  foreach ($objects as &$item) {
    // Remove fields that aren't necessary.
    unset($item['search_api_datasource'], $item['status']);

    foreach ($item as $name => &$field) {
      // Data that is being sent as the taxonomy term names should always be
      // sent as an array of strings. When the node is only configured with one
      // term in the field, it tries to send it as a string. So we force to be
      // an array.
      $property_path = $index->getField($name)?->getPropertyPath() ?: '';
      if (is_string($field) && str_contains($property_path, ':entity:name')) {
        $field = [$field];
      }

      // Either the canonical url hasn't been set, or it matches the current
      // request. It would match the current request when the event is happening
      // in the UI. If cron is running, the current host won't match the canonical
      // url.
      if (
        $site_domain &&
        $site_domain != $current_host &&
        is_string($field) &&
        str_contains($field, $current_host)
      ) {
        // Change the urls from the current host to the canonical url.
        $field = str_replace($current_host, $site_domain, $field);
      }
    }
  }
}