webform.module in Webform 6.x
Same filename and directory in other branches
Enables the creation of webforms and questionnaires.
File
webform.moduleView 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;
}