You are here

webform.module in Webform 6.x

Enables the creation of webforms and questionnaires.

File

webform.module
View source
<?php

/**
 * @file
 * Enables the creation of webforms and questionnaires.
 */
use Drupal\Component\Utility\Mail;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
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\Utility\WebformElementHelper;
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) {

  // 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();
}

/**
 * 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();
}

/**
 * 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) {
  module_load_include('inc', 'webform', '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>: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 ? Mail::formatDisplayName($params['from_name']) . ' <' . $params['from_mail'] . '>' : $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'] = $sender_name ? Mail::formatDisplayName($sender_name) . ' <' . $sender_mail . '>' : $sender_mail;
  }
}

/**
 * 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) {
  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.
  // @see admin_toolbar_tools_menu_links_discovered_alter()
  if (\Drupal::moduleHandler()
    ->moduleExists('admin_toolbar_tools')) {

    /** @var \Drupal\Core\Menu\LocalTaskManager $local_task_manager */
    $local_task_manager = \Drupal::service('plugin.manager.menu.local_task');
    $local_tasks = $local_task_manager
      ->getLocalTasks('entity.webform.collection', 0);
    foreach ($local_tasks['tabs'] as $local_task) {
      if (!isset($local_task['#link']['url']) || !$local_task['#link']['url'] instanceof Url) {
        continue;
      }
      $menu_item_title = $local_task['#link']['title'];
      $menu_item_route_name = $local_task['#link']['url']
        ->getRouteName();
      $menu_item_name = $menu_item_route_name . '.item';
      $links[$menu_item_name] = [
        'title' => $menu_item_title,
        'route_name' => $menu_item_route_name,
        'parent' => 'entity.webform.collection',
        'weight' => $local_task['#weight'],
        'provider' => 'webform',
        'menu_name' => 'admin',
      ];
      if (\Drupal::config('webform.settings')
        ->get('ui.toolbar_item')) {
        $local_sub_tasks = $local_task_manager
          ->getLocalTasks($menu_item_route_name, 1);
        foreach ($local_sub_tasks['tabs'] as $local_sub_task) {
          if (!$local_task['#link']['url'] instanceof Url) {
            continue;
          }
          $menu_sub_item_title = $local_sub_task['#link']['title'];
          $menu_sub_item_route_name = $local_sub_task['#link']['url']
            ->getRouteName();
          $menu_sub_item_name = $menu_sub_item_route_name . '.sub_item';
          $links[$menu_sub_item_name] = [
            'title' => $menu_sub_item_title,
            'route_name' => $menu_sub_item_route_name,
            'parent' => $menu_item_name,
            'weight' => $local_sub_task['#weight'],
            'provider' => 'webform',
            'menu_name' => 'admin',
          ];
        }
      }
    }
  }
}

/**
 * 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';
  }
}

/**
 * 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(drupal_get_path('module', 'webform') . "/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("\n(function(){\n  try {\n    if (window.sessionStorage) {\n      var key = 'Drupal.webform.{$webform->id()}.variant.{$element_key}';\n      window.sessionStorage.removeItem(key);\n    }\n  }\n  catch(e) {}\n})();\n"),
              '#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("\n(function(){\n\n  var hasSessionStorage = (function () {\n    try {\n      sessionStorage.setItem('webform', 'webform');\n      sessionStorage.removeItem('webform');\n      return true;\n    }\n    catch (e) {\n      return false;\n    }\n  }());\n\n  function getSessionVariantID(variant_key) {\n    if (hasSessionStorage) {\n      var key = 'Drupal.webform.{$webform->id()}.variant.' + variant_key;\n      return window.sessionStorage.getItem(key);\n    }\n    return null;\n  }\n\n  function setSessionVariantID(variant_key, variant_id) {\n    if (hasSessionStorage) {\n      var key = 'Drupal.webform.{$webform->id()}.variant.' + variant_key;\n      window.sessionStorage.setItem(key, variant_id);\n    }\n  }\n\n  var variants = {$json_variants};\n  var search = location.search;\n  var element_key, variant_ids, variant_id;\n  for (element_key in variants) {\n    if (variants.hasOwnProperty(element_key)\n      && !search.match(new RegExp('[?&]' + element_key + '='))) {\n        variant_ids = variants[element_key];\n        variant_id = getSessionVariantID(element_key);\n        if (!variant_ids.includes(variant_id)) {\n          variant_id = variant_ids[Math.floor(Math.random() * variant_ids.length)];\n          setSessionVariantID(element_key, variant_id);\n        }\n        search += (search ? '&' : '?') + element_key + '=' + variant_id;\n    }\n  }\n  if (search !== location.search) {\n    location.replace(location.pathname + search);\n  }\n})();\n"),
          '#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) {
  return ManagedFile::accessFileDownload($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.
 *
 * @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."),
      ],
    ],
  ];
}

/******************************************************************************/

// 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';
}

/**
 * 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;
      }
      list($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;
}

Functions

Namesort descending Description
webform_config_schema_info_alter Implements hook_config_schema_info_alter().
webform_contextual_links_view_alter Implements hook_contextual_links_view_alter().
webform_cron Implements hook_cron().
webform_devel_generate_info_alter Implements hook_devel_generate_info_alter().
webform_element_info_alter Implements hook_element_info_alter().
webform_entity_delete Implements hook_entity_delete().
webform_entity_update Implements hook_entity_update().
webform_file_access Implements hook_file_access().
webform_file_download Implements hook_file_download().
webform_file_validate_name_length Checks for files with names longer than can be stored in the database.
webform_help Implements hook_help().
webform_local_tasks_alter Implements hook_local_tasks_alter().
webform_mail Implements hook_mail().
webform_mail_alter Implements hook_mail_alter().
webform_menu_links_discovered_alter Implements hook_menu_links_discovered_alter().
webform_menu_local_tasks_alter Implements hook_menu_local_tasks_alter().
webform_modules_installed Implements hook_modules_installed().
webform_modules_uninstalled Implements hook_modules_uninstalled().
webform_module_implements_alter Implements hook_module_implements_alter().
webform_page_attachments Implements hook_page_attachments().
webform_process_options Process radios or checkboxes descriptions.
webform_rebuild Implements hook_rebuild().
webform_token_info_alter Implements hook_token_info_alter().
webform_toolbar_alter Implements hook_toolbar_alter().
webform_user_login Implements hook_user_login().
webform_webform_access_rules Implements hook_webform_access_rules().
webform_webform_message_custom Implements hook_webform_message_custom().
_webform_clear_webform_submission_list_cache_tag Invalidate 'webform_submission_list' cache tag when user or role is updated.
_webform_config_schema_info_alter_settings_recursive Convert most data types to 'string' to support tokens.
_webform_entity_element_validate_rendering_error_handler Provides custom PHP error handling when webform rendering is validated.
_webform_entity_element_validate_rendering_exception_handler Provides custom PHP exception handling when webform rendering is validated.
_webform_page_attachments Add webform libraries to page attachments.