<?php

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

use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\Core\Hook\Attribute\LegacyModuleImplementsAlter;
use Drupal\webform\Hook\WebformHooks;
use Drupal\Component\Serialization\Json;
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\file\FileInterface;
use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\Utility\WebformOptionsHelper;

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.tokens.inc';
require_once __DIR__ . '/includes/webform.translation.inc';
require_once __DIR__ . '/includes/webform.query.inc';

/**
 * Implements hook_help().
 */
#[LegacyHook]
function webform_help($route_name, RouteMatchInterface $route_match) {
  return \Drupal::service(WebformHooks::class)->help($route_name, $route_match);
}

/**
 * Implements hook_webform_message_custom().
 */
#[LegacyHook]
function webform_webform_message_custom($operation, $id) {
  \Drupal::service(WebformHooks::class)->webformMessageCustom($operation, $id);
}

/**
 * Implements hook_modules_installed().
 */
#[LegacyHook]
function webform_modules_installed($modules) {
  \Drupal::service(WebformHooks::class)->modulesInstalled($modules);
}

/**
 * Implements hook_modules_uninstalled().
 */
#[LegacyHook]
function webform_modules_uninstalled($modules) {
  \Drupal::service(WebformHooks::class)->modulesUninstalled($modules);
}

/**
 * Implements hook_config_schema_info_alter().
 */
#[LegacyHook]
function webform_config_schema_info_alter(&$definitions) {
  return \Drupal::service(WebformHooks::class)->configSchemaInfoAlter($definitions);
}

/**
 * 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().
 */
#[LegacyHook]
function webform_user_login($account) {
  \Drupal::service(WebformHooks::class)->userLogin($account);
}

/**
 * Implements hook_cron().
 */
#[LegacyHook]
function webform_cron() {
  \Drupal::service(WebformHooks::class)->cron();
}

/**
 * Implements hook_rebuild().
 */
#[LegacyHook]
function webform_rebuild() {
  \Drupal::service(WebformHooks::class)->rebuild();
}

/**
 * Implements hook_local_tasks_alter().
 */
#[LegacyHook]
function webform_local_tasks_alter(&$local_tasks) {
  \Drupal::service(WebformHooks::class)->localTasksAlter($local_tasks);
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
#[LegacyHook]
function webform_menu_local_tasks_alter(&$data, $route_name, RefinableCacheableDependencyInterface $cacheability) {
  \Drupal::service(WebformHooks::class)->menuLocalTasksAlter($data, $route_name, $cacheability);
}

/**
 * Implements hook_module_implements_alter().
 */
#[LegacyModuleImplementsAlter]
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().
 */
#[LegacyHook]
function webform_token_info_alter(&$data) {
  \Drupal::service(WebformHooks::class)->tokenInfoAlter($data);
}

/**
 * Implements hook_entity_update().
 */
#[LegacyHook]
function webform_entity_update(EntityInterface $entity) {
  \Drupal::service(WebformHooks::class)->entityUpdate($entity);
}

/**
 * Implements hook_entity_delete().
 */
#[LegacyHook]
function webform_entity_delete(EntityInterface $entity) {
  \Drupal::service(WebformHooks::class)->entityDelete($entity);
}

/**
 * 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().
 */
#[LegacyHook]
function webform_mail($key, &$message, $params) {
  \Drupal::service(WebformHooks::class)->mail($key, $message, $params);
}

/**
 * Implements hook_mail_alter().
 */
#[LegacyHook]
function webform_mail_alter(&$message) {
  \Drupal::service(WebformHooks::class)->mailAlter($message);
}

/**
 * Implements hook_toolbar_alter().
 */
#[LegacyHook]
function webform_toolbar_alter(&$items) {
  \Drupal::service(WebformHooks::class)->toolbarAlter($items);
}

/**
 * Implements hook_menu_links_discovered_alter().
 */
#[LegacyHook]
function webform_menu_links_discovered_alter(&$links) {
  \Drupal::service(WebformHooks::class)->menuLinksDiscoveredAlter($links);
}

/**
 * Implements hook_page_attachments().
 */
#[LegacyHook]
function webform_page_attachments(array &$attachments) {
  \Drupal::service(WebformHooks::class)->pageAttachments($attachments);
}

/**
 * Implements hook_metatags_alter().
 */
#[LegacyHook]
function webform_metatags_alter(array &$metatags, array &$context) {
  \Drupal::service(WebformHooks::class)->metatagsAlter($metatags, $context);
}

/**
 * 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 global CSS and JS.
  $config = \Drupal::config('webform.settings');
  if ($config->get('assets.css')) {
    $attachments['#attached']['library'][] = 'webform/webform.css';
  }
  if ($config->get('assets.javascript')) {
    $attachments['#attached']['library'][] = 'webform/webform.javascript';
  }

  // 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()
 */
#[LegacyHook]
function webform_file_access(FileInterface $file, $operation, AccountInterface $account) {
  return \Drupal::service(WebformHooks::class)->fileAccess($file, $operation, $account);
}

/**
 * Implements hook_file_download().
 */
#[LegacyHook]
function webform_file_download($uri) {
  return \Drupal::service(WebformHooks::class)->fileDownload($uri);
}

/**
 * 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.
 *
 * @deprecated in webform:6.3.0 and is removed from webform:7.0.0. Use
 *   'FileNameLength' file validator plugin instead.
 *
 * @see https://www.drupal.org/node/3475420
 * @see file_validate_name_length()
 */
function webform_file_validate_name_length(FileInterface $file) {
  @trigger_error(__FUNCTION__ . "() is deprecated in webform:6.3.0 and is removed from webform:7.0.0. Use 'FileNameLength' file validator plugin instead. See https://www.drupal.org/node/3475420", E_USER_DEPRECATED);
  $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
 */
#[LegacyHook]
function webform_contextual_links_view_alter(&$element, $items) {
  \Drupal::service(WebformHooks::class)->contextualLinksViewAlter($element, $items);
}

/**
 * Implements hook_webform_access_rules().
 */
#[LegacyHook]
function webform_webform_access_rules() {
  return \Drupal::service(WebformHooks::class)->webformAccessRules();
}

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

/**
 * Implements hook_element_info_alter().
 */
#[LegacyHook]
function webform_element_info_alter(array &$info) {
  \Drupal::service(WebformHooks::class)->elementInfoAlter($info);
}

/**
 * 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 form structure.
 *
 * @return array
 *   The processed element.
 */
function webform_process_options(&$element, FormStateInterface $form_state, &$complete_form) {
  if (!WebformElementHelper::isWebformElement($element)) {
    return $element;
  }

  // Set #webform_element for all options (checkboxes and radios).
  foreach (Element::children($element) as $key) {
    $element[$key]['#webform_element'] = TRUE;
  }

  // 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][$description_property_name] = $description;
    }
  }

  // Display as buttons.
  if (!empty($element['#options_display']) && str_starts_with($element['#options_display'], 'buttons')) {
    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;
}
