class WebformEntityElementsValidator in Webform 6.x
Same name and namespace in other branches
- 8.5 src/WebformEntityElementsValidator.php \Drupal\webform\WebformEntityElementsValidator
Webform elements validator.
Hierarchy
- class \Drupal\webform\WebformEntityElementsValidator implements WebformEntityElementsValidatorInterface uses StringTranslationTrait
Expanded class hierarchy of WebformEntityElementsValidator
1 string reference to 'WebformEntityElementsValidator'
1 service uses WebformEntityElementsValidator
File
- src/
WebformEntityElementsValidator.php, line 20
Namespace
Drupal\webformView source
class WebformEntityElementsValidator implements WebformEntityElementsValidatorInterface {
use StringTranslationTrait;
/**
* The webform being validated.
*
* @var \Drupal\webform\WebformInterface
*/
protected $webform;
/**
* The raw elements value.
*
* @var string
*/
protected $elementsRaw;
/**
* The raw original elements value.
*
* @var string
*/
protected $originalElementsRaw;
/**
* The parsed elements array.
*
* @var array
*/
protected $elements;
/**
* The parsed original elements array.
*
* @var array
*/
protected $originalElements;
/**
* An array of element keys.
*
* @var array
*/
protected $elementKeys;
/**
* The configuration object factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The 'renderer' service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The 'plugin.manager.webform.element' service.
*
* @var \Drupal\webform\Plugin\WebformElementManagerInterface
*/
protected $elementManager;
/**
* The 'entity_type.manager' service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The 'form_builder' service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* Element keys/names that are reserved.
*
* @var array
*/
public static $reservedNames = [
'add',
'form_build_id',
'form_id',
'form_token',
'op',
];
/**
* Constructs a WebformEntityElementsValidator object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration object factory.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The 'renderer' service.
* @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
* The 'plugin.manager.webform.element' service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The 'entity_type.manager' service.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The 'form_builder' service.
*/
public function __construct(ConfigFactoryInterface $config_factory, RendererInterface $renderer, WebformElementManagerInterface $element_manager, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder) {
$this->configFactory = $config_factory;
$this->renderer = $renderer;
$this->elementManager = $element_manager;
$this->entityTypeManager = $entity_type_manager;
$this->formBuilder = $form_builder;
}
/**
* {@inheritdoc}
*/
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();
// Validate required.
if ($options['required'] && ($message = $this
->validateRequired())) {
return [
$message,
];
}
// Validate contain valid YAML.
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);
}
// Validate elements are an array.
if ($options['array'] && ($message = $this
->validateArray())) {
return [
$message,
];
}
// Validate duplicate element name.
if ($options['names']) {
if ($messages = $this
->validateNames()) {
return $messages;
}
elseif ($messages = $this
->validateDuplicateNames()) {
return $messages;
}
}
// Validate ignored properties.
if ($options['properties'] && ($messages = $this
->validateProperties())) {
return $messages;
}
// Validate submission data.
if ($options['submissions'] && ($messages = $this
->validateSubmissions())) {
return $messages;
}
// Validate variants data.
if ($options['variants'] && ($messages = $this
->validateVariants())) {
return $messages;
}
// Validate hierarchy.
if ($options['hierarchy'] && ($messages = $this
->validateHierarchy())) {
return $messages;
}
// Validate pages.
if ($options['pages'] && ($messages = $this
->validatePages())) {
return $messages;
}
// Validate rendering.
if ($options['rendering'] && ($message = $this
->validateRendering())) {
return [
$message,
];
}
return NULL;
}
/**
* Validate elements are required.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
* If not valid an error message.
*/
protected function validateRequired() {
return empty($this->elementsRaw) ? $this
->t('Elements are required') : NULL;
}
/**
* Validate elements is validate YAML.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
* If not valid an error message.
*/
protected function validateYaml() {
try {
WebformYaml::decode($this->elementsRaw);
return NULL;
} catch (\Exception $exception) {
return $this
->t('Elements are not valid. @message', [
'@message' => $exception
->getMessage(),
]);
}
}
/**
* Validate elements are an array of elements.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
* If not valid an error message.
*/
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;
}
/**
* Validate elements names.
*
* @return array|null
* If not valid, an array of error messages.
*/
protected function validateNames() {
// @see \Drupal\webform_ui\Form\WebformUiElementFormBase::buildForm
$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;
}
/**
* Validate elements does not contain duplicate names.
*
* @return array|null
* If not valid, an array of error 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;
}
/**
* Recurse through elements and collect an associative array keyed by name and number of duplicate instances.
*
* @param array $elements
* An array of elements.
* @param array $names
* An associative array keyed by name and number of duplicate instances.
*/
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);
}
}
/**
* Validate that elements are not using ignored properties.
*
* @return array|null
* If not valid, an array of error messages.
*/
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;
}
/**
* Validate that element are not deleted when the webform has submissions.
*
* @return array|null
* If not valid, an array of error messages.
*/
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) {
// Display an error message with 3 possible approaches to safely
// deleting or hiding an element.
$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;
}
/**
* Validate that element are not deleted when the webform has related variants.
*
* @return array|null
* If not valid, an array of error messages.
*/
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;
}
/**
* Validate element hierarchy.
*
* @return array|null
* If not valid, an array of error messages.
*/
protected function validateHierarchy() {
$elements = $this->webform
->getElementsInitializedAndFlattened();
$messages = [];
foreach ($elements as $key => $element) {
$plugin_id = $this->elementManager
->getElementPluginId($element);
/** @var \Drupal\webform\Plugin\WebformElementInterface $webform_element */
$webform_element = $this->elementManager
->createInstance($plugin_id);
$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;
}
/**
* Validate wizard/card pages.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|string|null
* If not valid an error message.
*
* @see \Drupal\Core\Entity\EntityFormBuilder
* @see \Drupal\webform\Entity\Webform::getSubmissionForm()
*/
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;
}
}
/**
* Validate that elements are a valid render array.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|string|null
* If not valid an error message.
*
* @see \Drupal\Core\Entity\EntityFormBuilder
* @see \Drupal\webform\Entity\Webform::getSubmissionForm()
*/
protected function validateRendering() {
// Override Drupal's error and exception handler so that we can capture
// all rendering exceptions and display the captured error/exception
// message to the user.
// @see _webform_entity_element_validate_rendering_error_handler()
// @see _webform_entity_element_validate_rendering_exception_handler()
set_error_handler('_webform_entity_element_validate_rendering_error_handler');
set_exception_handler('_webform_entity_element_validate_rendering_exception_handler');
try {
/** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
$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 Drupal's error and exception handler.
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;
}
/****************************************************************************/
// Helper methods.
/****************************************************************************/
/**
* Recurse through elements and collect an associative array of deleted element keys.
*
* @param array $elements
* An array of elements.
* @param array $names
* An array tracking deleted element keys.
*/
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);
}
}
/**
* Get the line numbers for given pattern in the webform's elements string.
*
* @param string $pattern
* A regular expression.
*
* @return array
* An array of line numbers.
*/
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;
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
StringTranslationTrait:: |
protected | property | The string translation service. | 4 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. | |
WebformEntityElementsValidator:: |
protected | property | The configuration object factory. | |
WebformEntityElementsValidator:: |
protected | property | An array of element keys. | |
WebformEntityElementsValidator:: |
protected | property | The 'plugin.manager.webform.element' service. | |
WebformEntityElementsValidator:: |
protected | property | The parsed elements array. | |
WebformEntityElementsValidator:: |
protected | property | The raw elements value. | |
WebformEntityElementsValidator:: |
protected | property | The 'entity_type.manager' service. | |
WebformEntityElementsValidator:: |
protected | property | The 'form_builder' service. | |
WebformEntityElementsValidator:: |
protected | property | The parsed original elements array. | |
WebformEntityElementsValidator:: |
protected | property | The raw original elements value. | |
WebformEntityElementsValidator:: |
protected | property | The 'renderer' service. | |
WebformEntityElementsValidator:: |
public static | property | Element keys/names that are reserved. | |
WebformEntityElementsValidator:: |
protected | property | The webform being validated. | |
WebformEntityElementsValidator:: |
protected | function | Recurse through elements and collect an associative array keyed by name and number of duplicate instances. | |
WebformEntityElementsValidator:: |
protected | function | Recurse through elements and collect an associative array of deleted element keys. | |
WebformEntityElementsValidator:: |
protected | function | Get the line numbers for given pattern in the webform's elements string. | |
WebformEntityElementsValidator:: |
public | function |
Validate webform elements. Overrides WebformEntityElementsValidatorInterface:: |
|
WebformEntityElementsValidator:: |
protected | function | Validate elements are an array of elements. | |
WebformEntityElementsValidator:: |
protected | function | Validate elements does not contain duplicate names. | |
WebformEntityElementsValidator:: |
protected | function | Validate element hierarchy. | |
WebformEntityElementsValidator:: |
protected | function | Validate elements names. | |
WebformEntityElementsValidator:: |
protected | function | Validate wizard/card pages. | |
WebformEntityElementsValidator:: |
protected | function | Validate that elements are not using ignored properties. | |
WebformEntityElementsValidator:: |
protected | function | Validate that elements are a valid render array. | |
WebformEntityElementsValidator:: |
protected | function | Validate elements are required. | |
WebformEntityElementsValidator:: |
protected | function | Validate that element are not deleted when the webform has submissions. | |
WebformEntityElementsValidator:: |
protected | function | Validate that element are not deleted when the webform has related variants. | |
WebformEntityElementsValidator:: |
protected | function | Validate elements is validate YAML. | |
WebformEntityElementsValidator:: |
public | function | Constructs a WebformEntityElementsValidator object. |