View source
<?php
namespace Drupal\webform;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Form\FormState;
use Drupal\Core\Url;
use Drupal\webform\Plugin\WebformElementManagerInterface;
use Drupal\webform\Utility\WebformArrayHelper;
use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\Utility\WebformYaml;
class WebformEntityElementsValidator implements WebformEntityElementsValidatorInterface {
use StringTranslationTrait;
protected $webform;
protected $elementsRaw;
protected $originalElementsRaw;
protected $elements;
protected $originalElements;
protected $elementKeys;
protected $renderer;
protected $elementManager;
protected $entityTypeManager;
protected $formBuilder;
protected $configFactory;
public static $reservedNames = [
'add',
'form_build_id',
'form_id',
'form_token',
'op',
];
public function __construct(RendererInterface $renderer, WebformElementManagerInterface $element_manager, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, ConfigFactoryInterface $config_factory = NULL) {
$this->renderer = $renderer;
$this->elementManager = $element_manager;
$this->entityTypeManager = $entity_type_manager;
$this->formBuilder = $form_builder;
$this->configFactory = $config_factory ?: \Drupal::configFactory();
}
public function validate(WebformInterface $webform, array $options = []) {
$options += [
'required' => TRUE,
'yaml' => TRUE,
'array' => TRUE,
'names' => TRUE,
'properties' => TRUE,
'submissions' => TRUE,
'variants' => TRUE,
'hierarchy' => TRUE,
'pages' => TRUE,
'rendering' => TRUE,
];
$this->webform = $webform;
$this->elementsRaw = $webform
->getElementsRaw();
$this->originalElementsRaw = $webform
->getElementsOriginalRaw();
if ($options['required'] && ($message = $this
->validateRequired())) {
return [
$message,
];
}
if ($options['yaml'] && ($message = $this
->validateYaml())) {
return [
$message,
];
}
$this->elements = WebformYaml::decode($this->elementsRaw);
$this->originalElements = WebformYaml::decode($this->originalElementsRaw);
$this->elementKeys = [];
if (is_array($this->elements)) {
$this
->getElementKeysRecursive($this->elements, $this->elementKeys);
}
if ($options['array'] && ($message = $this
->validateArray())) {
return [
$message,
];
}
if ($options['names']) {
if ($messages = $this
->validateNames()) {
return $messages;
}
elseif ($messages = $this
->validateDuplicateNames()) {
return $messages;
}
}
if ($options['properties'] && ($messages = $this
->validateProperties())) {
return $messages;
}
if ($options['submissions'] && ($messages = $this
->validateSubmissions())) {
return $messages;
}
if ($options['variants'] && ($messages = $this
->validateVariants())) {
return $messages;
}
if ($options['hierarchy'] && ($messages = $this
->validateHierarchy())) {
return $messages;
}
if ($options['pages'] && ($messages = $this
->validatePages())) {
return $messages;
}
if ($options['rendering'] && ($message = $this
->validateRendering())) {
return [
$message,
];
}
return NULL;
}
protected function validateRequired() {
return empty($this->elementsRaw) ? $this
->t('Elements are required') : NULL;
}
protected function validateYaml() {
try {
WebformYaml::decode($this->elementsRaw);
return NULL;
} catch (\Exception $exception) {
return $this
->t('Elements are not valid. @message', [
'@message' => $exception
->getMessage(),
]);
}
}
protected function validateArray() {
if (!is_array($this->elements)) {
return $this
->t('Elements are not valid. YAML must contain an associative array of elements.');
}
return NULL;
}
protected function validateNames() {
$machine_name_pattern = $this->configFactory
->get('webform.settings')
->get('element.machine_name_pattern') ?: 'a-z0-9_';
switch ($machine_name_pattern) {
case 'a-z0-9_':
$machine_name_requirement = $this
->t('lowercase letters, numbers, and underscores');
break;
case 'a-zA-Z0-9_':
$machine_name_requirement = $this
->t('letters, numbers, and underscores');
break;
case 'a-z0-9_-':
$machine_name_requirement = $this
->t('lowercase letters, numbers, underscores, and dashes');
break;
case 'a-zA-Z0-9_-':
$machine_name_requirement = $this
->t('letters, numbers, underscores, and dashes');
break;
}
$messages = [];
foreach ($this->elementKeys as $name) {
if (!preg_match('/^[' . $machine_name_pattern . ']+$/', $name)) {
$line_numbers = $this
->getLineNumbers('/^\\s*(["\']?)' . preg_quote($name, '/') . '\\1\\s*:/');
$t_args = [
'%name' => $name,
'@line_number' => WebformArrayHelper::toString($line_numbers),
'@requirement' => $machine_name_requirement,
];
$messages[] = $this
->t('The element key %name on line @line_number must contain only @requirement.', $t_args);
}
elseif (in_array($name, static::$reservedNames)) {
$line_numbers = $this
->getLineNumbers('/^\\s*(["\']?)' . preg_quote($name, '/') . '\\1\\s*:/');
$t_args = [
'%name' => $name,
'@line_number' => WebformArrayHelper::toString($line_numbers),
];
$messages[] = $this
->t('The element key %name on line @line_number is a reserved key.', $t_args);
}
}
return $messages;
}
protected function validateDuplicateNames() {
$duplicate_names = [];
$this
->getDuplicateNamesRecursive($this->elements, $duplicate_names);
if ($duplicate_names = array_filter($duplicate_names)) {
$messages = [];
foreach ($duplicate_names as $duplicate_name => $duplicate_count) {
$line_numbers = $this
->getLineNumbers('/^\\s*(["\']?)' . preg_quote($duplicate_name, '/') . '\\1\\s*:/');
$t_args = [
'%name' => $duplicate_name,
'@line_numbers' => WebformArrayHelper::toString($line_numbers),
];
$messages[] = $this
->formatPlural(count($line_numbers), 'Elements contain a duplicate element key %name found on line @line_numbers.', 'Elements contain a duplicate element key %name found on lines @line_numbers.', $t_args);
}
return $messages;
}
return NULL;
}
protected function getDuplicateNamesRecursive(array $elements, array &$names) {
foreach ($elements as $key => &$element) {
if (!WebformElementHelper::isElement($element, $key)) {
continue;
}
if (isset($element['#type'])) {
if (!isset($names[$key])) {
$names[$key] = 0;
}
else {
++$names[$key];
}
}
$this
->getDuplicateNamesRecursive($element, $names);
}
}
protected function validateProperties() {
$ignored_properties = WebformElementHelper::getIgnoredProperties($this->elements);
if ($ignored_properties) {
$messages = [];
foreach ($ignored_properties as $ignored_property => $ignored_message) {
if ($ignored_property !== $ignored_message) {
$messages[] = $ignored_message;
}
else {
$line_numbers = $this
->getLineNumbers('/^\\s*(["\']?)' . preg_quote($ignored_property, '/') . '\\1\\s*:/');
$t_args = [
'%property' => $ignored_property,
'@line_number' => WebformArrayHelper::toString($line_numbers),
];
$messages[] = $this
->formatPlural(count($line_numbers), 'Elements contain an unsupported %property property found on line @line_number.', 'Elements contain an unsupported %property property found on lines @line_number.', $t_args);
}
}
return $messages;
}
return NULL;
}
protected function validateSubmissions() {
if (!$this->webform
->hasSubmissions()) {
return NULL;
}
$element_keys = [];
if ($this->elements) {
$this
->getElementKeysRecursive($this->elements, $element_keys);
}
$original_element_keys = [];
if ($this->originalElements) {
$this
->getElementKeysRecursive($this->originalElements, $original_element_keys);
}
if ($missing_element_keys = array_diff_key($original_element_keys, $element_keys)) {
$messages = [];
foreach ($missing_element_keys as $missing_element_key) {
$items = [];
$items[] = $this
->t('<a href=":href">Delete all submissions</a> to this webform.', [
':href' => $this->webform
->toUrl('results-clear')
->toString(),
]);
if (\Drupal::moduleHandler()
->moduleExists('webform_ui')) {
$items[] = $this
->t('<a href=":href">Delete this individual element</a> using the webform UI.', [
':href' => Url::fromRoute('entity.webform_ui.element.delete_form', [
'webform' => $this->webform
->id(),
'key' => $missing_element_key,
])
->toString(),
]);
}
else {
$items[] = $this
->t('<a href=":href">Enable the Webform UI module</a> and safely delete this element.', [
':href' => Url::fromRoute('system.modules_list')
->toString(),
]);
}
$items[] = $this
->t("Hide this element by setting its <code>'#access'</code> property to <code>false</code>.");
$build = [
'message' => [
'#markup' => $this
->t('The %key element can not be removed because the %title webform has <a href=":href">results</a>.', [
'%title' => $this->webform
->label(),
'%key' => $missing_element_key,
':href' => $this->webform
->toUrl('results-submissions')
->toString(),
]),
],
'items' => [
'#theme' => 'item_list',
'#items' => $items,
],
];
$messages[] = $this->renderer
->renderPlain($build);
}
return $messages;
}
return NULL;
}
protected function validateVariants() {
if (!$this->webform
->hasVariants()) {
return NULL;
}
$element_keys = [];
if ($this->elements) {
$this
->getElementKeysRecursive($this->elements, $element_keys);
}
$original_element_keys = [];
if ($this->originalElements) {
$this
->getElementKeysRecursive($this->originalElements, $original_element_keys);
}
if ($missing_element_keys = array_diff_key($original_element_keys, $element_keys)) {
$messages = [];
foreach ($missing_element_keys as $missing_element_key) {
if ($this->webform
->getVariants(NULL, NULL, $missing_element_key)
->count()) {
$t_args = [
'%title' => $this->webform
->label(),
'%key' => $missing_element_key,
':href' => $this->webform
->toUrl('variants')
->toString(),
];
$messages[] = $this
->t('The %key element can not be removed because the %title webform has related <a href=":href">variants</a>.', $t_args);
}
}
return $messages;
}
return NULL;
}
protected function validateHierarchy() {
$elements = $this->webform
->getElementsInitializedAndFlattened();
$messages = [];
foreach ($elements as $key => $element) {
$plugin_id = $this->elementManager
->getElementPluginId($element);
$webform_element = $this->elementManager
->createInstance($plugin_id, $element);
$t_args = [
'%title' => !empty($element['#title']) ? $element['#title'] : $key,
'@type' => $webform_element
->getTypeName(),
];
if ($webform_element
->isRoot() && !empty($element['#webform_parent_key'])) {
$messages[] = $this
->t('The %title (@type) is a root element that can not be used as child to another element', $t_args);
}
elseif (!$webform_element
->isContainer($element) && !empty($element['#webform_children'])) {
$messages[] = $this
->t('The %title (@type) is a webform element that can not have any child elements.', $t_args);
}
elseif ($plugin_id === 'webform_table_row') {
$parent_element = $element['#webform_parent_key'] ? $elements[$element['#webform_parent_key']] : NULL;
if (!$parent_element || !isset($parent_element['#type']) || $parent_element['#type'] !== 'webform_table') {
$t_args += [
'%parent_title' => $this
->t('Table'),
'@parent_type' => 'webform_table',
];
$messages[] = $this
->t('The %title (@type) must be with in a %parent_title (@parent_type) element.', $t_args);
}
}
}
return $messages;
}
protected function validatePages() {
if (strpos($this->elementsRaw, "'#type': webform_card") !== FALSE && strpos($this->elementsRaw, "'#type': webform_wizard_page") !== FALSE) {
return [
$this
->t('Pages and cards cannot be used in the same webform. Please remove or convert the pages/cards to the same element type.'),
];
}
else {
return NULL;
}
}
protected function validateRendering() {
set_error_handler('_webform_entity_element_validate_rendering_error_handler');
set_exception_handler('_webform_entity_element_validate_rendering_exception_handler');
try {
$webform_submission = $this->entityTypeManager
->getStorage('webform_submission')
->create([
'webform' => $this->webform,
]);
$form_object = $this->entityTypeManager
->getFormObject('webform_submission', 'add');
$form_object
->setEntity($webform_submission);
$form_state = (new FormState())
->setFormState([]);
$this->formBuilder
->buildForm($form_object, $form_state);
$message = NULL;
} catch (\Throwable $error) {
$message = $error
->getMessage();
} catch (\Exception $exception) {
$message = $exception
->getMessage();
}
restore_error_handler();
restore_exception_handler();
if ($message) {
$build = [
'title' => [
'#markup' => $this
->t('Unable to render elements, please view the below message(s) and the error log.'),
],
'items' => [
'#theme' => 'item_list',
'#items' => [
$message,
],
],
];
return $this->renderer
->renderPlain($build);
}
return $message;
}
protected function getElementKeysRecursive(array $elements, array &$names) {
foreach ($elements as $key => &$element) {
if (!WebformElementHelper::isElement($element, $key)) {
continue;
}
if (isset($element['#type'])) {
$names[$key] = $key;
}
$this
->getElementKeysRecursive($element, $names);
}
}
protected function getLineNumbers($pattern) {
$lines = explode(PHP_EOL, $this->elementsRaw);
$line_numbers = [];
foreach ($lines as $index => $line) {
if (preg_match($pattern, $line)) {
$line_numbers[] = $index + 1;
}
}
return $line_numbers;
}
}