<?php

/**
 * @file
 * Enables the creation of webforms and questionnaires.
 */

use Drupal\Component\Serialization\Json;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\webform\Entity\Webform;
use Drupal\webform\Entity\WebformSubmission;
use Drupal\webform\Plugin\WebformElement\ManagedFile;
use Drupal\webform\Plugin\WebformElementFileDownloadAccessInterface;
use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\Utility\WebformMailHelper;
use Drupal\webform\Utility\WebformOptionsHelper;
use Drupal\webform\WebformInterface;

require_once __DIR__ . '/includes/webform.date.inc';
require_once __DIR__ . '/includes/webform.editor.inc';
require_once __DIR__ . '/includes/webform.form_alter.inc';
require_once __DIR__ . '/includes/webform.libraries.inc';
require_once __DIR__ . '/includes/webform.options.inc';
require_once __DIR__ . '/includes/webform.theme.inc';
require_once __DIR__ . '/includes/webform.translation.inc';
require_once __DIR__ . '/includes/webform.query.inc';

/**
 * Implements hook_help().
 */
function webform_help($route_name, RouteMatchInterface $route_match) {
  if (!$route_match->getRouteObject()) {
    return NULL;
  }

  // Get path from route match.
  $path = preg_replace('/^' . preg_quote(base_path(), '/') . '/', '/', Url::fromRouteMatch($route_match)->setAbsolute(FALSE)->toString());
  if (!in_array($route_name, ['system.modules_list', 'update.status']) && strpos($route_name, 'webform') === FALSE && strpos($path, '/webform') === FALSE) {
    return NULL;
  }

  /** @var \Drupal\webform\WebformHelpManagerInterface $help_manager */
  $help_manager = \Drupal::service('webform.help_manager');
  if ($route_name === 'help.page.webform') {
    $build = $help_manager->buildIndex();
  }
  else {
    $build = $help_manager->buildHelp($route_name, $route_match);
  }

  if ($build) {
    $renderer = \Drupal::service('renderer');
    $config = \Drupal::config('webform.settings');
    $renderer->addCacheableDependency($build, $config);
    return $build;
  }
  else {
    return NULL;
  }
}

/**
 * Implements hook_webform_message_custom().
 */
function webform_webform_message_custom($operation, $id) {
  if (strpos($id, 'webform_help_notification__') === 0 && $operation === 'close') {
    $id = str_replace('webform_help_notification__', '', $id);
    /** @var \Drupal\webform\WebformHelpManagerInterface $help_manager */
    $help_manager = \Drupal::service('webform.help_manager');
    $help_manager->deleteNotification($id);
  }
}

/**
 * Implements hook_modules_installed().
 */
function webform_modules_installed($modules) {
  // Add webform paths when the path.module is being installed.
  if (in_array('path', $modules)) {
    /** @var \Drupal\webform\WebformInterface[] $webforms */
    $webforms = Webform::loadMultiple();
    foreach ($webforms as $webform) {
      $webform->updatePaths();
    }
  }

  // Check HTML email provider support as modules are installed.
  /** @var \Drupal\webform\WebformEmailProviderInterface $email_provider */
  $email_provider = \Drupal::service('webform.email_provider');
  $email_provider->check();

  // Update Webform HTML editor.
  if (in_array('ckeditor', $modules)
    || in_array('ckeditor5', $modules)) {
    \Drupal::moduleHandler()->loadInclude('webform', 'inc', 'includes/webform.install') .
    _webform_update_html_editor();
  }
}

/**
 * Implements hook_modules_uninstalled().
 */
function webform_modules_uninstalled($modules) {
  // Remove uninstalled module's third party settings from admin settings.
  $config = \Drupal::configFactory()->getEditable('webform.settings');
  $third_party_settings = $config->get('third_party_settings');

  $has_third_party_settings = FALSE;
  foreach ($modules as $module) {
    if (isset($third_party_settings[$module])) {
      $has_third_party_settings = TRUE;
      unset($third_party_settings[$module]);
    }
  }
  if ($has_third_party_settings) {
    $config->set('third_party_settings', $third_party_settings);
    $config->save();
  }

  // Check HTML email provider support as modules are uninstalled.
  /** @var \Drupal\webform\WebformEmailProviderInterface $email_provider */
  $email_provider = \Drupal::service('webform.email_provider');
  $email_provider->check();

  // Update Webform HTML editor.
  if (in_array('ckeditor', $modules)
    || in_array('ckeditor5', $modules)) {
    \Drupal::moduleHandler()->loadInclude('webform', 'inc', 'includes/webform.install') .
    _webform_update_html_editor();
  }
}

/**
 * Implements hook_config_schema_info_alter().
 */
function webform_config_schema_info_alter(&$definitions) {
  if (empty($definitions['webform.webform.*']['mapping'])) {
    return;
  }
  $mapping = $definitions['webform.webform.*']['mapping'];

  // Copy setting, elements, and handlers to variant override schema.
  if (isset($definitions['webform.variant.override'])) {
    $definitions['webform.variant.override']['mapping'] += [
      'settings' => $mapping['settings'],
      'elements' => $mapping['elements'],
      'handlers' => $mapping['handlers'],
    ];
  }

  // Append settings handler settings schema.
  if (isset($definitions['webform.handler.settings'])) {
    $definitions['webform.handler.settings']['mapping'] += _webform_config_schema_info_alter_settings_recursive($mapping['settings']['mapping']);
  }
}

/**
 * Convert most data types to 'string' to support tokens.
 *
 * @param array $settings
 *   An associative array of schema settings.
 *
 * @return array
 *   An associative array of schema settings with most data types to 'string'
 *   to support tokens
 */
function _webform_config_schema_info_alter_settings_recursive(array $settings) {
  foreach ($settings as $name => $setting) {
    if (is_array($setting)) {
      $settings[$name] = _webform_config_schema_info_alter_settings_recursive($setting);
    }
    elseif ($name === 'type' && in_array($setting, ['boolean', 'integer', 'float', 'uri', 'email'])) {
      $settings[$name] = 'string';
    }
  }
  return $settings;
}

/**
 * Implements hook_user_login().
 */
function webform_user_login($account) {
  // Notify the storage of this log in.
  \Drupal::entityTypeManager()->getStorage('webform_submission')->userLogin($account);
}

/**
 * Implements hook_cron().
 */
function webform_cron() {
  $config = \Drupal::config('webform.settings');
  \Drupal::entityTypeManager()->getStorage('webform_submission')->purge($config->get('purge.cron_size'));
}

/**
 * Implements hook_rebuild().
 */
function webform_rebuild() {
  /** @var \Drupal\webform\WebformEmailProviderInterface $email_provider */
  $email_provider = \Drupal::service('webform.email_provider');
  $email_provider->check();
}

/**
 * Implements hook_local_tasks_alter().
 */
function webform_local_tasks_alter(&$local_tasks) {
  // Change config translation local task hierarchy.
  if (isset($local_tasks['config_translation.local_tasks:entity.webform.config_translation_overview'])) {
    $local_tasks['config_translation.local_tasks:entity.webform.config_translation_overview']['base_route'] = 'entity.webform.canonical';
  }
  if (isset($local_tasks['config_translation.local_tasks:config_translation.item.overview.webform.config'])) {
    // Set weight to 110 so that the 'Translate' tab comes after
    // the 'Advanced' tab.
    // @see webform.links.task.yml
    $local_tasks['config_translation.local_tasks:config_translation.item.overview.webform.config']['weight'] = 110;
    $local_tasks['config_translation.local_tasks:config_translation.item.overview.webform.config']['parent_id'] = 'webform.config';
  }

  // Disable 'Contribute' tab if explicitly disabled or the Contribute module
  // is installed.
  if (\Drupal::config('webform.settings')->get('ui.contribute_disabled') || \Drupal::moduleHandler()->moduleExists('contribute')) {
    unset($local_tasks['webform.contribute']);
  }
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function webform_menu_local_tasks_alter(&$data, $route_name, RefinableCacheableDependencyInterface $cacheability) {
  // Change config entities 'Translate *' tab to be just label 'Translate'.
  $webform_entities = [
    'webform',
    'webform_options',
  ];
  foreach ($webform_entities as $webform_entity) {
    if (isset($data['tabs'][0]["config_translation.local_tasks:entity.$webform_entity.config_translation_overview"]['#link']['title'])) {
      $data['tabs'][0]["config_translation.local_tasks:entity.$webform_entity.config_translation_overview"]['#link']['title'] = t('Translate');
    }
  }

  // Change simple config 'Translate *' tab to be just label 'Translate'.
  if (isset($data['tabs'][1]['config_translation.local_tasks:config_translation.item.overview.webform.config'])) {
    $data['tabs'][1]['config_translation.local_tasks:config_translation.item.overview.webform.config']['#link']['title'] = t('Translate');
  }

  // ISSUE:
  // Devel routes do not use 'webform' parameter which throws the below error.
  // Some mandatory parameters are missing ("webform") to generate a URL for
  // route "entity.webform_submission.canonical"
  //
  // WORKAROUND:
  // Make sure webform parameter is set for all routes.
  if (strpos($route_name, 'entity.webform_submission.devel_') === 0 || $route_name === 'entity.webform_submission.token_devel') {
    foreach ($data['tabs'] as $tab_level) {
      foreach ($tab_level as $tab) {
        /** @var \Drupal\Core\Url $url */
        $url = $tab['#link']['url'];
        $tab_route_name = $url->getRouteName();
        $tab_route_parameters = $url->getRouteParameters();

        if (strpos($tab_route_name, 'entity.webform_submission.devel_') !== 0) {
          $webform_submission = WebformSubmission::load($tab_route_parameters['webform_submission']);
          $url->setRouteParameter('webform', $webform_submission->getWebform()->id());
        }
      }
    }
  }

  // Allow webform query string parameters to be transferred
  // from a canonical URL to a test URL.
  //
  // Please note: This behavior is only applicable when a user can
  // test a webform.
  $route_names = [
    'entity.webform.test_form' => 'entity.webform.canonical',
    'entity.node.webform.test_form' => 'entity.node.canonical',
  ];
  if (in_array($route_name, $route_names) || array_key_exists($route_name, $route_names)) {
    $query = \Drupal::request()->query->all();
    $has_test_tab = FALSE;
    foreach ($route_names as $test_route_name => $view_route_name) {
      if (isset($data['tabs'][0][$test_route_name])) {
        $has_test_tab = TRUE;
        if ($query) {
          $data['tabs'][0][$test_route_name]['#link']['url']
            ->setOption('query', $query);
          $data['tabs'][0][$view_route_name]['#link']['url']
            ->setOption('query', $query);
        }
      }
    }
    // Query string to cache context webform canonical and test routes.
    if ($has_test_tab) {
      $cacheability->addCacheContexts(['url.query_args']);
    }
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function webform_module_implements_alter(&$implementations, $hook) {
  if ($hook === 'form_alter') {
    $implementation = $implementations['webform'];
    unset($implementations['webform']);
    $implementations['webform'] = $implementation;
  }
}

/**
 * Implements hook_token_info_alter().
 */
function webform_token_info_alter(&$data) {
  \Drupal::moduleHandler()->loadInclude('webform', 'tokens.inc');

  // Append learn more about token suffixes to all webform token descriptions.
  // @see \Drupal\webform\WebformTokenManager::replace
  // @see webform_page_attachments()
  $token_suffixes = t('Append the below suffixes to alter the returned value.') .
    '<ul>' .
    '<li>' . t('<code>:base64encode</code> base64 encodes returned value') . '</li>' .
    '<li>' . t('<code>:clear</code> removes the token when it is not replaced.') . '</li>' .
    '<li>' . t('<code>:urlencode</code> URL encodes returned value.') . '</li>' .
    '<li>' . t('<code>:rawurlencode</code> Raw URL encodes returned value with only hex digits.') . '</li>' .
    '<li>' . t('<code>:xmlencode</code> XML encodes returned value.') . '</li>' .
    '<li>' . t('<code>:htmldecode</code> decodes HTML entities in returned value.') . '<br/><b>' . t('This suffix has security implications.') . '</b><br/>' . t('Use <code>:htmldecode</code> with <code>:striptags</code>.') . '</li>' .
    '<li>' . t('<code>:striptags</code> removes all HTML tags from returned value.') . '</li>' .
  '</ul>';
  $more = _webform_token_render_more(t('Learn about token suffixes'), $token_suffixes);
  foreach ($data['types'] as $type => &$info) {
    if (strpos($type, 'webform') === 0) {
      if (isset($info['description']) && !empty($info['description'])) {
        $description = $info['description'] . $more;
      }
      else {
        $description = $more;
      }
      $info['description'] = Markup::create($description);
    }
  }
}

/**
 * Implements hook_entity_update().
 */
function webform_entity_update(EntityInterface $entity) {
  _webform_clear_webform_submission_list_cache_tag($entity);
}

/**
 * Implements hook_entity_delete().
 */
function webform_entity_delete(EntityInterface $entity) {
  _webform_clear_webform_submission_list_cache_tag($entity);

  /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
  $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');

  // Delete saved export settings for a webform or source entity with the
  // webform field.
  if (($entity instanceof WebformInterface) || $entity_reference_manager->hasField($entity)) {
    $name = 'webform.export.' . $entity->getEntityTypeId() . '.' . $entity->id();
    \Drupal::state()->delete($name);
  }
}

/**
 * Invalidate 'webform_submission_list' cache tag when user or role is updated.
 *
 * Once the below issue is resolved we should rework this approach.
 *
 * Issue #2811041: Allow views base tables to define additional
 * cache tags and max age.
 * https://www.drupal.org/project/drupal/issues/2811041
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   An entity.
 *
 * @see \Drupal\webform\Entity\WebformSubmission
 * @see webform_query_webform_submission_access_alter()
 */
function _webform_clear_webform_submission_list_cache_tag(EntityInterface $entity) {
  if ($entity->getEntityTypeId() === 'user') {
    $original_target_ids = [];
    if ($entity->original) {
      foreach ($entity->original->roles as $item) {
        $original_target_ids[$item->target_id] = $item->target_id;
      }
    }
    $target_ids = [];
    foreach ($entity->roles as $item) {
      $target_ids[$item->target_id] = $item->target_id;
    }
    if (array_diff_assoc($original_target_ids, $target_ids)) {
      Cache::invalidateTags(['webform_submission_list']);
    }
  }
  elseif ($entity->getEntityTypeId() === 'user_role') {
    Cache::invalidateTags(['webform_submission_list']);
  }
}

/**
 * Implements hook_mail().
 */
function webform_mail($key, &$message, $params) {
  // Never send emails when using devel generate to create
  // 1000's of submissions.
  if (\Drupal::moduleHandler()->moduleExists('devel_generate')) {
    /** @var \Drupal\devel_generate\DevelGeneratePluginManager $devel_generate */
    $devel_generate = \Drupal::service('plugin.manager.develgenerate');
    $definition = $devel_generate->getDefinition('webform_submission', FALSE);
    if ($definition) {
      $class = $definition['class'];
      if ($class::isGeneratingSubmissions()) {
        $message['send'] = FALSE;
      }
    }
  }

  // Set default parameters.
  $params += [
    'from_mail' => '',
    'from_name' => '',
    'cc_mail' => '',
    'bcc_mail' => '',
    'reply_to' => '',
    'return_path' => '',
    'sender_mail' => '',
    'sender_name' => '',
  ];

  $message['subject'] = $params['subject'];
  $message['body'][] = $params['body'];

  // Set the header 'From'.
  // Using the 'from_mail' so that the webform's email from value is used
  // instead of site's email address.
  // @see: \Drupal\Core\Mail\MailManager::mail.
  if (!empty($params['from_mail'])) {
    // 'From name' is only used when the 'From mail' contains a single
    // email address.
    $from = (!empty($params['from_name']) && strpos($params['from_mail'], ',') === FALSE)
      ? WebformMailHelper::formatAddress($params['from_mail'], $params['from_name'])
      : $params['from_mail'];
    $message['from'] = $message['headers']['From'] = $from;
  }

  // Set header 'Cc'.
  if (!empty($params['cc_mail'])) {
    $message['headers']['Cc'] = $params['cc_mail'];
  }

  // Set header 'Bcc'.
  if (!empty($params['bcc_mail'])) {
    $message['headers']['Bcc'] = $params['bcc_mail'];
  }

  // Set header 'Reply-to'.
  $reply_to = $params['reply_to'] ?: '';
  if (empty($reply_to) && !empty($params['from_mail'])) {
    $reply_to = $message['from'];
  }
  if ($reply_to) {
    $message['reply-to'] = $message['headers']['Reply-to'] = $reply_to;
  }

  // Set header 'Return-Path' which only supports a single email address and the
  // 'from_mail' may contain multiple comma delimited email addresses.
  $return_path = $params['return_path'] ?: $params['from_mail'] ?: '';
  if ($return_path) {
    $return_path = explode(',', $return_path);
    $message['headers']['Sender'] = $message['headers']['Return-Path'] = $return_path[0];
  }

  // Set header 'Sender'.
  $sender_mail = $params['sender_mail'] ?: '';
  $sender_name = $params['sender_name'] ?: $params['from_name'] ?: '';
  if ($sender_mail) {
    $message['headers']['Sender'] = WebformMailHelper::formatAddress($sender_mail, $sender_name);
  }
}

/**
 * Implements hook_mail_alter().
 */
function webform_mail_alter(&$message) {
  // Drupal hardcodes all mail header as 'text/plain' so we need to set the
  // header's 'Content-type' to HTML if the EmailWebformHandler's
  // 'html' flag has been set.
  // @see \Drupal\Core\Mail\MailManager::mail()
  // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::getMessage().
  if (strpos($message['id'], 'webform') === 0) {
    if (isset($message['params']['html']) && $message['params']['html']) {
      $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed';
    }
  }
}

/**
 * Implements hook_toolbar_alter().
 */
function webform_toolbar_alter(&$items) {
  if (\Drupal::config('webform.settings')->get('ui.toolbar_item')) {
    $items['administration']['#attached']['library'][] = 'webform/webform.admin.toolbar';
  }
}

/**
 * Implements hook_menu_links_discovered_alter().
 */
function webform_menu_links_discovered_alter(&$links) {
  // Display Webforms as a top-level administration menu item in the toolbar.
  if (\Drupal::config('webform.settings')->get('ui.toolbar_item')) {
    $links['entity.webform.collection']['parent'] = 'system.admin';
    $links['entity.webform.collection']['weight'] = -9;
  }

  // Add webform local tasks as admin menu toolbar menu items.
  if (\Drupal::moduleHandler()->moduleExists('admin_toolbar_tools')) {
    // Get local task definitions.
    /** @var \Drupal\Core\Menu\LocalTaskManager $local_task_manager */
    $local_task_manager = \Drupal::service('plugin.manager.menu.local_task');
    $definitions = $local_task_manager->getDefinitions();

    // Set default definition.
    $default_definition = [
      'provider' => 'webform',
      'menu_name' => 'admin',
    ];

    // Get keys to be copied.
    $keys = ['title', 'route_name', 'weight'];
    $keys_to_copy = array_combine($keys, $keys);

    $menu_links = [];
    foreach ($definitions as $task_name => $definition) {
      if (isset($definition['base_route']) && $definition['base_route'] === 'entity.webform.collection') {
        $menu_links[$task_name . '.item'] = $default_definition
          + array_intersect_key($definition, $keys_to_copy)
          + ['parent' => 'entity.webform.collection'];
      }
    }
    foreach ($menu_links as $sub_link_task_name => $sub_link) {
      foreach ($definitions as $task_name => $definition) {
        if (isset($definition['parent_id']) && $definition['parent_id'] === preg_replace('/\.item$/', '', $sub_link_task_name)) {
          $menu_links[$task_name . '.item'] = $default_definition
            + array_intersect_key($definition, $keys_to_copy)
            + ['parent' => $sub_link_task_name];
        }
      }
    }

    // Make sure weight are integers and not floats which throw fatal errors
    // for PostgreSQL.
    // @see https://www.drupal.org/project/webform/issues/3247861
    // @see https://www.drupal.org/project/drupal/issues/3248199
    foreach ($menu_links as &$menu_link) {
      $menu_link['weight'] = (int) $menu_link['weight'];
    }

    $links += $menu_links;
  }
}

/**
 * Implements hook_page_attachments().
 */
function webform_page_attachments(array &$attachments) {
  $route_name = \Drupal::routeMatch()->getRouteName();

  // Attach global libraries only to webform specific pages and module list.
  if (preg_match('/^(webform\.|^entity\.([^.]+\.)?webform)/', $route_name)
    || $route_name === 'system.modules_list') {
    _webform_page_attachments($attachments);
  }

  // Attach codemirror and select2 library to block admin to ensure that the
  // library is loaded by the webform block is placed using Ajax.
  if (strpos($route_name, 'block.admin_display') === 0) {
    $attachments['#attached']['library'][] = 'webform/webform.block';
  }

  // Attach webform dialog library and options to every page.
  if (\Drupal::config('webform.settings')->get('settings.dialog')) {
    $attachments['#attached']['library'][] = 'webform/webform.dialog';
    $attachments['#attached']['drupalSettings']['webform']['dialog']['options'] = \Drupal::config('webform.settings')->get('settings.dialog_options');
    /** @var \Drupal\webform\WebformRequestInterface $request_handler */
    $request_handler = \Drupal::service('webform.request');
    if ($source_entity = $request_handler->getCurrentSourceEntity()) {
      $attachments['#attached']['drupalSettings']['webform']['dialog']['entity_type'] = $source_entity->getEntityTypeId();
      $attachments['#attached']['drupalSettings']['webform']['dialog']['entity_id'] = $source_entity->id();
    }
  }

  // Attach webform more element to token token help.
  // @see webform_token_info_alter()
  if ($route_name === 'help.page' && \Drupal::routeMatch()->getRawParameter('name') === 'token') {
    $attachments['#attached']['library'][] = 'webform/webform.token';
  }

  // Attach meta tag robots noindex directive to all webform confirmation pages
  // , if the metatag module is not installed.
  // @see webform_metatags_alter()
  if (!\Drupal::moduleHandler()->moduleExists('metatag')
    && !empty(\Drupal::config('webform.settings')->get('settings.default_confirmation_noindex'))
    && preg_match('/^(entity\.webform\.confirmation|entity\.[a-z-_]+\.webform\.confirmation)$/', $route_name)) {
    $attachments['#attached']['html_head'][] = [
      [
        '#tag' => 'meta',
        '#attributes' => ['name' => 'robots', 'content' => 'noindex'],
      ],
      'webform_confirmation_noindex',
    ];
  }
}

/**
 * Implements hook_metatags_alter().
 */
function webform_metatags_alter(array &$metatags, array &$context) {
  $route_name = \Drupal::routeMatch()->getRouteName();
  if (!empty(\Drupal::config('webform.settings')->get('settings.default_confirmation_noindex'))
    && $route_name
    && preg_match('/^(entity\.webform\.confirmation|entity\.[a-z-_]+\.webform\.confirmation)$/', $route_name)) {
    $robots = (!empty($metatags['robots']))
      ? preg_split('/\s*,\s*/', $metatags['robots'])
      : [];
    $robots[] = 'noindex';
    $metatags['robots'] = implode(', ', array_unique($robots));
  }
}

/**
 * Add webform libraries to page attachments.
 *
 * @param array $attachments
 *   An array of page attachments.
 */
function _webform_page_attachments(array &$attachments) {
  // Attach webform theme specific libraries.
  /** @var \Drupal\webform\WebformThemeManagerInterface $theme_manager */
  $theme_manager = \Drupal::service('webform.theme_manager');
  $active_theme_names = $theme_manager->getActiveThemeNames();
  foreach ($active_theme_names as $active_theme_name) {
    if (file_exists(__DIR__ . "/css/webform.theme.$active_theme_name.css")) {
      $attachments['#attached']['library'][] = "webform/webform.theme.$active_theme_name";
    }
  }

  // Attach webform contextual link helper.
  if (\Drupal::currentUser()->hasPermission('access contextual links')) {
    $attachments['#attached']['library'][] = 'webform/webform.contextual';
  }

  // Attach details element save open/close library.
  // This ensures pages without a webform will still be able to save the
  // details element state.
  if (\Drupal::config('webform.settings')->get('ui.details_save')) {
    $attachments['#attached']['library'][] = 'webform/webform.element.details.save';
  }

  // Add 'info' message style to all webform pages.
  $attachments['#attached']['library'][] = 'webform/webform.element.message';

  // Get current webform, if it does not exist exit.
  /** @var \Drupal\webform\WebformRequestInterface $request_handler */
  $request_handler = \Drupal::service('webform.request');
  $webform = $request_handler->getCurrentWebform();
  if (!$webform) {
    return;
  }

  // Assets: Add custom shared and webform specific CSS and JS.
  // @see webform_library_info_build()
  $assets = $webform->getAssets();
  foreach ($assets as $type => $value) {
    if ($value) {
      $attachments['#attached']['library'][] = 'webform/webform.' . $type . '.' . $webform->id();
    }
  }

  // Attach variant randomization JavaScript.
  $route_name = \Drupal::routeMatch()->getRouteName();
  $route_names = [
    'entity.webform.canonical',
    'entity.webform.test_form',
    'entity.node.canonical',
    'entity.node.webform.test_form',
    // Webform Share module routes.
    'entity.webform.share_page',
    'entity.webform.share_page.javascript',
  ];
  if (in_array($route_name, $route_names)) {
    $variants = [];
    $element_keys = $webform->getElementsVariant();
    foreach ($element_keys as $element_key) {
      $element = $webform->getElement($element_key);
      if (!empty($element['#prepopulate']) && !empty($element['#randomize'])) {
        $variant_plugins = $webform->getVariants(NULL, TRUE, $element_key);
        if ($variant_plugins->count()) {
          $variants[$element_key] = array_values($variant_plugins->getInstanceIds());
        }
        else {
          $attachments['#attached']['html_head'][] = [
            [
              '#type' => 'html_tag',
              '#tag' => 'script',
              '#value' => Markup::create("
(function(){
  try {
    if (window.sessionStorage) {
      var key = 'Drupal.webform.{$webform->id()}.variant.{$element_key}';
      window.sessionStorage.removeItem(key);
    }
  }
  catch(e) {}
})();
"),
              '#weight' => 1000,
            ],
            'webform_variant_' . $element_key . '_clear',
          ];
        }
      }
    }

    if ($variants) {
      // Using JavaScript for redirection allows pages to be cached
      // by URL with querystring parameters.
      $json_variants = Json::encode($variants);
      $attachments['#attached']['html_head'][] = [
        [
          '#type' => 'html_tag',
          '#tag' => 'script',
          '#value' => Markup::create("
(function(){

  var hasSessionStorage = (function () {
    try {
      sessionStorage.setItem('webform', 'webform');
      sessionStorage.removeItem('webform');
      return true;
    }
    catch (e) {
      return false;
    }
  }());

  function getSessionVariantID(variant_key) {
    if (hasSessionStorage) {
      var key = 'Drupal.webform.{$webform->id()}.variant.' + variant_key;
      return window.sessionStorage.getItem(key);
    }
    return null;
  }

  function setSessionVariantID(variant_key, variant_id) {
    if (hasSessionStorage) {
      var key = 'Drupal.webform.{$webform->id()}.variant.' + variant_key;
      window.sessionStorage.setItem(key, variant_id);
    }
  }

  var variants = $json_variants;
  var search = location.search;
  var element_key, variant_ids, variant_id;
  for (element_key in variants) {
    if (variants.hasOwnProperty(element_key)
      && !search.match(new RegExp('[?&]' + element_key + '='))) {
        variant_ids = variants[element_key];
        variant_id = getSessionVariantID(element_key);
        if (!variant_ids.includes(variant_id)) {
          variant_id = variant_ids[Math.floor(Math.random() * variant_ids.length)];
          setSessionVariantID(element_key, variant_id);
        }
        search += (search ? '&' : '?') + element_key + '=' + variant_id;
    }
  }
  if (search !== location.search) {
    location.replace(location.pathname + search);
  }
})();
"),
          '#weight' => 1000,
        ],
        'webform_variant_randomize',
      ];
    }
  }
}

/**
 * Implements hook_file_access().
 *
 * @see file_file_download()
 * @see webform_preprocess_file_link()
 */
function webform_file_access(FileInterface $file, $operation, AccountInterface $account) {
  $is_webform_download = ($operation === 'download' && strpos($file->getFileUri(), 'private://webform/') === 0);

  // Block access to temporary anonymous private file uploads
  // only when an anonymous user is attempting to download the file.
  // Links to anonymous file uploads are automatically suppressed.
  // @see webform_preprocess_file_link()
  // @see webform_file_download()
  if ($is_webform_download
    && $file->isTemporary()
    && $file->getOwner() && $file->getOwner()->isAnonymous()
    && \Drupal::routeMatch()->getRouteName() === 'system.files') {
    return AccessResult::forbidden();
  }

  // Allow access to files associated with a webform submission.
  // This prevent uploaded webform files from being lost when another user
  // edits a submission with multiple file uploads.
  // @see \Drupal\file\Element\ManagedFile::valueCallback
  if ($is_webform_download && ManagedFile::accessFile($file, $account)) {
    return AccessResult::allowed();
  }

  return AccessResult::neutral();
}

/**
 * Implements hook_file_download().
 */
function webform_file_download($uri) {
  /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $webform_element_manager */
  $webform_element_manager = \Drupal::service('plugin.manager.webform.element');
  $webform_elements = $webform_element_manager->getInstances();
  foreach ($webform_elements as $webform_element) {
    if ($webform_element->isEnabled() && $webform_element instanceof WebformElementFileDownloadAccessInterface) {
      $result = $webform_element::accessFileDownload($uri);
      if ($result !== NULL) {
        return $result;
      }
    }
  }
  return NULL;
}

/**
 * Checks for files with names longer than can be stored in the database.
 *
 * @param \Drupal\file\FileInterface $file
 *   A file entity.
 *
 * @return array
 *   An empty array if the file name length is smaller than the limit or an
 *   array containing an error message if it's not or is empty.
 *
 * @see file_validate_name_length()
 */
function webform_file_validate_name_length(FileInterface $file) {
  $errors = [];
  // Don't display error is the file_validate_name_length() has already
  // displayed a warning because the files length is over 240.
  if (strlen($file->getFilename()) > 240) {
    return $errors;
  }
  if (strlen($file->getFilename()) > 150) {
    $errors[] = t("The file's name exceeds the Webform module's 150 characters limit. Please rename the file and try again.");
  }
  return $errors;
}

/**
 * Implements hook_contextual_links_view_alter().
 *
 * Add .webform-contextual class to all webform context links.
 *
 * @see webform.links.contextual.yml
 * @see js/webform.contextual.js
 */
function webform_contextual_links_view_alter(&$element, $items) {
  $links = [
    'entitywebformtest-form',
    'entitywebformresults-submissions',
    'entitywebformedit-form',
    'entitywebformsettings',
  ];
  foreach ($links as $link) {
    if (isset($element['#links'][$link])) {
      $element['#links'][$link]['attributes']['class'][] = 'webform-contextual';
    }
  }
}

/**
 * Implements hook_webform_access_rules().
 */
function webform_webform_access_rules() {
  return [
    'create' => [
      'title' => t('Create submissions'),
      'roles' => [
        'anonymous',
        'authenticated',
      ],
    ],
    'view_any' => [
      'title' => t('View any submissions'),
    ],
    'update_any' => [
      'title' => t('Update any submissions'),
    ],
    'delete_any' => [
      'title' => t('Delete any submissions'),
    ],
    'purge_any' => [
      'title' => t('Purge any submissions'),
    ],
    'view_own' => [
      'title' => t('View own submissions'),
    ],
    'update_own' => [
      'title' => t('Update own submissions'),
    ],
    'delete_own' => [
      'title' => t('Delete own submissions'),
    ],
    'administer' => [
      'title' => t('Administer webform & submissions'),
      'description' => [
        '#type' => 'webform_message',
        '#message_type' => 'warning',
        '#message_message' => t('<strong>Warning</strong>: The below settings give users, permissions, and roles full access to this webform and its submissions.'),
      ],
    ],
    'test' => [
      'title' => t('Test webform'),
    ],
    'configuration' => [
      'title' => t('Access webform configuration'),
      'description' => [
        '#type' => 'webform_message',
        '#message_type' => 'warning',
        '#message_message' => t("<strong>Warning</strong>: The below settings give users, permissions, and roles full access to this webform's configuration via API requests."),
      ],
    ],
  ];
}

/**
 * Implements hook_field_info_alter().
 */
function webform_field_info_alter(&$info) {
  // @todo Remove once Drupal 10.2.x is only supported.
  if (floatval(\Drupal::VERSION) < 10.2) {
    $info['webform']['category'] = new TranslatableMarkup("Reference");
  }
}

/* ************************************************************************** */
// Devel generate info hooks.
/* ************************************************************************** */

/**
 * Implements hook_devel_generate_info_alter().
 */
function webform_devel_generate_info_alter(array &$generators) {
  if (!isset($generators['webform_submission'])) {
    return;
  }

  // Use deprecated generator because the devel_generate.module changed the
  // DevelGenerateBaseInterface.
  //
  // @see \Drupal\webform\Plugin\DevelGenerate\WebformSubmissionDevelGenerateDeprecated
  // @see https://www.drupal.org/project/webform/issues/3155654
  // @see https://gitlab.com/drupalspoons/devel/-/issues/324
  $info = \Drupal::service('extension.list.module')->getExtensionInfo('devel_generate');
  if (!empty($info['version']) && strpos($info['version'], '8.x-') === 0) {
    $generators['webform_submission']['class'] = 'Drupal\webform\Plugin\DevelGenerate\WebformSubmissionDevelGenerateDeprecated';
  }
}

/* ************************************************************************** */
// Element info hooks.
/* ************************************************************************** */

/**
 * Implements hook_element_info_alter().
 */
function webform_element_info_alter(array &$info) {
  $info['checkboxes']['#process'][] = 'webform_process_options';
  $info['radios']['#process'][] = 'webform_process_options';
  $info['webform_entity_checkboxes']['#process'][] = 'webform_process_options';
  $info['webform_entity_radios']['#process'][] = 'webform_process_options';

  if (isset($info['text_format'])) {
    $info['text_format']['#process'] = array_merge(
      $info['text_format']['#process'] ?? [],
      [['\Drupal\webform\Element\WebformHtmlEditor', 'processTextFormat']]
    );
    $info['text_format']['#pre_render'] = array_merge(
      [['\Drupal\webform\Element\WebformHtmlEditor', 'preRenderTextFormat']],
      $info['text_format']['#pre_render'] ?? []
    );
  }
  if (isset($info['processed_text'])) {
    $info['processed_text']['#pre_render'][] = [
      '\Drupal\webform\Element\WebformHtmlEditor',
      'preRenderProcessedText',
    ];
  }
}

/**
 * Process radios or checkboxes descriptions.
 *
 * @param array $element
 *   An associative array containing the properties and children of the
 *   radios or checkboxes element.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 * @param array $complete_form
 *   The complete webform structure.
 *
 * @return array
 *   The processed element.
 */
function webform_process_options(&$element, FormStateInterface $form_state, &$complete_form) {
  if (!WebformElementHelper::isWebformElement($element)) {
    return $element;
  }

  // Description display.
  if (!empty($element['#options_description_display'])) {
    $description_property_name = ($element['#options_description_display'] === 'help') ? '#help' : '#description';
    foreach (Element::children($element) as $key) {
      $title = (string) $element[$key]['#title'];
      // Check for -- delimiter.
      if (!WebformOptionsHelper::hasOptionDescription($title)) {
        continue;
      }

      [$title, $description] = WebformOptionsHelper::splitOption($title);
      $element[$key]['#title'] = $title;
      $element[$key]['#webform_element'] = TRUE;
      $element[$key][$description_property_name] = $description;
    }
  }

  // Display as buttons.
  if (!empty($element['#options_display']) && strpos($element['#options_display'], 'buttons') === 0) {
    foreach (Element::children($element) as $key) {
      // Add wrapper which is needed to make flexbox work with tables.
      $element[$key]['#prefix'] = '<div class="webform-options-display-buttons-wrapper">';
      $element[$key]['#suffix'] = '</div>';

      // Move radio #description inside the #title (aka label).
      if (!empty($element[$key]['#description'])) {
        $build = [
          'title' => [
            '#markup' => $element[$key]['#title'],
            '#prefix' => '<div class="webform-options-display-buttons-title">',
            '#suffix' => '</div>',
          ],
          'description' => [
            '#markup' => $element[$key]['#description'],
            '#prefix' => '<div class="webform-options-display-buttons-description description">',
            '#suffix' => '</div>',
          ],
        ];
        $element[$key]['#title'] = \Drupal::service('renderer')->render($build);
        unset($element[$key]['#description']);
      }

      // Add .visually-hidden class radio/checkbox.
      $element[$key]['#attributes']['class'][] = 'visually-hidden';

      // Add class to label attributes.
      $element[$key]['#label_attributes']['class'][] = 'webform-options-display-buttons-label';

      // Add #option_display to button.
      // @see \Drupal\webform_bootstrap_test_theme\Plugin\Preprocess\FormElement::preprocessElement
      $element[$key]['#option_display'] = 'button';

      // Add webform element property to trigger radio/checkbox template suggestions.
      // @see webform_theme_suggestions_form_element()
      $element[$key]['#webform_element'] = TRUE;
    }
  }

  // Issue #2839344: Some aria-describedby refers to not existing element ID.
  // @see https://www.drupal.org/project/drupal/issues/2839344
  if (!empty($element['#attributes']['aria-describedby'])) {
    foreach (Element::children($element) as $key) {
      if (empty($element[$key]['#attributes']['aria-describedby'])
        && $element['#attributes']['aria-describedby'] === $element[$key]['#attributes']['aria-describedby']) {
        unset($element[$key]['#attributes']['aria-describedby']);
      }
    }
  }

  return $element;
}

/* ************************************************************************** */
// Private functions.
/* ************************************************************************** */

/**
 * Provides custom PHP error handling when webform rendering is validated.
 *
 * Converts E_RECOVERABLE_ERROR to WARNING so that an exceptions can be thrown
 * and caught by
 * \Drupal\webform\WebformEntityElementsValidator::validateRendering().
 *
 * @param int $error_level
 *   The level of the error raised.
 * @param string $message
 *   The error message.
 * @param string $filename
 *   (optional) The filename that the error was raised in.
 * @param string $line
 *   (optional) The line number the error was raised at.
 * @param array $context
 *   (optional) An array that points to the active symbol table at the point the
 *   error occurred.
 *
 * @throws \ErrorException
 *   Throw ErrorException for E_RECOVERABLE_ERROR errors.
 *
 * @see \Drupal\webform\WebformEntityElementsValidator::validateRendering()
 */
function _webform_entity_element_validate_rendering_error_handler($error_level, $message, $filename = NULL, $line = NULL, $context = NULL) {
  // From: http://stackoverflow.com/questions/15461611/php-try-catch-not-catching-all-exceptions
  if (E_RECOVERABLE_ERROR === $error_level) {
    // Allow Drupal to still log the error but convert it to a warning.
    _drupal_error_handler(E_WARNING, $message, $filename, $line, $context);
    throw new ErrorException($message, $error_level, 0, $filename, $line);
  }
  else {
    _drupal_error_handler($error_level, $message, $filename, $line, $context);
  }
}

/**
 * Provides custom PHP exception handling when webform rendering is validated.
 *
 * @param \Exception|\Throwable $exception
 *   The exception object that was thrown.
 *
 * @throws \Exception
 *   Throw the exception back to
 *   WebformEntityElementsValidator::validateRendering().
 *
 * @see \Drupal\webform\WebformEntityElementsValidator::validateRendering()
 */
function _webform_entity_element_validate_rendering_exception_handler($exception) {
  throw $exception;
}
