View source
<?php
namespace Drupal\webform;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\webform\Plugin\WebformElement\TextBase;
use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
use Drupal\webform\Plugin\WebformElement\WebformElement;
use Drupal\webform\Plugin\WebformElementManagerInterface;
use Drupal\webform\Utility\WebformArrayHelper;
use Drupal\webform\Utility\WebformElementHelper;
class WebformSubmissionConditionsValidator implements WebformSubmissionConditionsValidatorInterface {
use StringTranslationTrait;
protected $aliases = [
'enabled' => '!disabled',
'invisible' => '!visible',
'invisible-slide' => '!visible-slide',
'invalid' => '!valid',
'optional' => '!required',
'filled' => '!empty',
'unchecked' => '!checked',
'expanded' => '!collapsed',
'open' => '!collapsed',
'closed' => 'collapsed',
'readwrite' => '!readonly',
];
protected $elementManager;
public function __construct(WebformElementManagerInterface $element_manager) {
$this->elementManager = $element_manager;
}
public function buildPages(array $pages, WebformSubmissionInterface $webform_submission) {
foreach ($pages as $page_key => $page) {
if ($page['#access'] === FALSE) {
unset($pages[$page_key]);
}
if (!empty($page['#states'])) {
$state = key($page['#states']);
$conditions = $page['#states'][$state];
$result = $this
->validateState($state, $conditions, $webform_submission);
if ($result !== NULL && !$result) {
unset($pages[$page_key]);
}
}
}
return $pages;
}
public function buildForm(array &$form, FormStateInterface $form_state) {
$webform_submission = $form_state
->getFormObject()
->getEntity();
$visible_elements =& $this
->getBuildElements($form);
foreach ($visible_elements as &$element) {
$states =& WebformElementHelper::getStates($element);
$element['#_webform_states'] = $states;
foreach ($states as $original_state => $conditions) {
if (!is_array($conditions)) {
continue;
}
list($state, $negate) = $this
->processState($original_state);
if (strpos($state, 'visible') === 0) {
$element['#after_build'][] = [
get_class($this),
'elementAfterBuild',
];
}
$targets = $this
->getConditionTargetsVisibility($conditions, $visible_elements);
$all_targets_visible = array_sum($targets) === count($targets);
$has_cross_page_targets = !$all_targets_visible && array_sum($targets);
if ($all_targets_visible) {
if (strpos($state, 'visible') === 0 && !$this
->validateConditions($conditions, $webform_submission)) {
$this
->addStatesHiddenToElement($element);
}
continue;
}
if ($has_cross_page_targets) {
$cross_page_targets = array_filter($targets, function ($visible) {
return $visible === FALSE;
});
$states[$original_state] = $this
->replaceCrossPageTargets($conditions, $webform_submission, $cross_page_targets, $form);
continue;
}
$result = $this
->validateConditions($conditions, $webform_submission);
if ($result === NULL) {
continue;
}
$result = $negate ? !$result : $result;
switch ($state) {
case 'required':
$element['#required'] = $result;
break;
case 'readonly':
if ($result) {
$element['#attributes']['readonly'] = 'readonly';
$element['#wrapper_attributes']['class'][] = 'webform-readonly';
}
break;
case 'disabled':
$element['#disabled'] = $result;
break;
case 'visible':
case 'visible-slide':
if (!$result) {
$this
->addStatesHiddenToElement($element);
if (!isset($element['#states_clear']) || $element['#states_clear'] === TRUE) {
unset($element['#default_value']);
}
}
break;
case 'collapsed':
$element['#open'] = !$result;
break;
case 'checked':
$element['#default_value'] = $result;
break;
}
unset($states[$original_state]);
}
if (empty($states)) {
unset($element['#states']);
}
}
}
public function replaceCrossPageTargets(array $conditions, WebformSubmissionInterface $webform_submission, array $targets, array &$form) {
static $cross_page_values = [];
$cross_page_conditions = [];
foreach ($conditions as $index => $value) {
if (is_int($index) && is_array($value) && WebformArrayHelper::isSequential($value)) {
$cross_page_conditions[$index] = $this
->replaceCrossPageTargets($value, $webform_submission, $targets, $form);
}
else {
$cross_page_conditions[$index] = $value;
if (is_string($value) && in_array($value, [
'and',
'or',
'xor',
])) {
continue;
}
elseif (is_int($index)) {
$selector = key($value);
$condition = $value[$selector];
}
else {
$selector = $index;
$condition = $value;
}
if (!isset($targets[$selector])) {
continue;
}
$condition_result = $this
->validateCondition($selector, $condition, $webform_submission);
if ($condition_result === NULL) {
continue;
}
$target_trigger = $condition_result ? 'value' : '!value';
$target_name = 'webform_states_' . Crypt::hashBase64($selector);
$target_selector = ':input[name="' . $target_name . '"]';
if (!isset($cross_page_values[$target_name])) {
$cross_page_values[$target_name] = rand();
}
$target_value = $cross_page_values[$target_name];
if (is_int($index)) {
unset($cross_page_conditions[$index][$selector]);
$cross_page_conditions[$index][$target_selector] = [
$target_trigger => $target_value,
];
}
else {
unset($cross_page_conditions[$selector]);
$cross_page_conditions[$target_selector] = [
$target_trigger => $target_value,
];
}
$form[$target_name] = [
'#type' => 'hidden',
'#value' => $target_value,
];
}
}
return $cross_page_conditions;
}
public function validateForm(array &$form, FormStateInterface $form_state) {
$this
->validateFormRecursive($form, $form_state);
}
protected function validateFormRecursive(array $form, FormStateInterface $form_state) {
foreach ($form as $key => $element) {
if (!WebformElementHelper::isElement($element, $key) || !WebformElementHelper::isAccessibleElement($element)) {
continue;
}
$this
->validateFormElement($element, $form_state);
$this
->validateFormRecursive($element, $form_state);
}
}
protected function validateFormElement(array $element, FormStateInterface $form_state) {
$states = WebformElementHelper::getStates($element);
if (empty($states)) {
return;
}
$webform_submission = $form_state
->getFormObject()
->getEntity();
foreach ($states as $state => $conditions) {
if (!in_array($state, [
'required',
'optional',
])) {
continue;
}
$element_plugin = $this->elementManager
->getElementInstance($element);
if (!$element_plugin
->isInput($element)) {
continue;
}
$is_required = $this
->validateConditions($conditions, $webform_submission);
$is_required = $state === 'optional' ? !$is_required : $is_required;
if (!$is_required) {
continue;
}
if (isset($element['#webform_key'])) {
$value = $webform_submission
->getElementData($element['#webform_key']);
}
else {
$value = $element['#value'];
}
$element_definition = $element_plugin
->getFormElementClassDefinition();
if (method_exists($element_definition, 'setRequiredError')) {
$element_definition::setRequiredError($element, $form_state);
}
else {
$is_empty = empty($value) && $value !== '0';
$is_default_input_mask = TextBase::isDefaultInputMask($element, $value);
if ($is_empty || $is_default_input_mask) {
WebformElementHelper::setRequiredError($element, $form_state);
}
}
}
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$webform_submission = $form_state
->getFormObject()
->getEntity();
$current_page = $form_state
->get('current_page');
$check_access = !$current_page || $current_page === WebformInterface::PAGE_CONFIRMATION ? FALSE : TRUE;
$data = $webform_submission
->getData();
$this
->submitFormRecursive($form, $webform_submission, $data, $check_access);
$webform_submission
->setData($data);
}
protected function submitFormRecursive(array $elements, WebformSubmissionInterface $webform_submission, array &$data, $check_access, $visible = TRUE) {
foreach ($elements as $key => &$element) {
if (!WebformElementHelper::isElement($element, $key)) {
continue;
}
if (isset($element['#states_clear']) && $element['#states_clear'] === FALSE) {
continue;
}
if ($check_access && isset($element['#_webform_access']) && $element['#_webform_access'] === FALSE) {
continue;
}
$element_visible = $visible && $this
->isElementVisible($element, $webform_submission) ? TRUE : FALSE;
if (!$element_visible && !empty($element['#webform_key']) && isset($data[$key])) {
$data[$key] = is_array($data[$key]) ? [] : '';
}
$this
->submitFormRecursive($element, $webform_submission, $data, $check_access, $element_visible);
}
}
public static function elementAfterBuild(array $element, FormStateInterface $form_state) {
return WebformElementHelper::setElementValidate($element, [
get_called_class(),
'elementValidate',
]);
}
public static function elementValidate(array &$element, FormStateInterface $form_state) {
$form_object = $form_state
->getFormObject();
$complete_form =& $form_state
->getCompleteForm();
$webform_submission = $form_object
->buildEntity($complete_form, $form_state);
$conditions_validator = \Drupal::service('webform_submission.conditions_validator');
if ($conditions_validator
->isElementVisible($element, $webform_submission)) {
WebformElementHelper::triggerElementValidate($element, $form_state);
}
else {
WebformElementHelper::suppressElementValidate($element, $form_state);
}
}
public function isElementVisible(array $element, WebformSubmissionInterface $webform_submission) {
$states = WebformElementHelper::getStates($element);
$visible = TRUE;
foreach ($states as $state => $conditions) {
if (!is_array($conditions)) {
continue;
}
list($state, $negate) = $this
->processState($state);
$result = $this
->validateConditions($conditions, $webform_submission);
if ($result === NULL) {
continue;
}
$result = $negate ? !$result : $result;
if (strpos($state, 'visible') === 0 && $result === FALSE) {
$visible = FALSE;
}
}
return $visible;
}
public function isElementEnabled(array $element, WebformSubmissionInterface $webform_submission) {
$states = WebformElementHelper::getStates($element);
$enabled = TRUE;
foreach ($states as $state => $conditions) {
if (!is_array($conditions)) {
continue;
}
list($state, $negate) = $this
->processState($state);
$result = $this
->validateConditions($conditions, $webform_submission);
if ($result === NULL) {
continue;
}
$result = $negate ? !$result : $result;
if ($state === 'disabled' && $result === TRUE) {
$enabled = FALSE;
}
}
return $enabled;
}
public function validateState($state, array $conditions, WebformSubmissionInterface $webform_submission) {
list($state, $negate) = $this
->processState($state);
$result = $this
->validateConditions($conditions, $webform_submission);
if ($result === NULL) {
return NULL;
}
return $negate ? !$result : $result;
}
public function validateConditions(array $conditions, WebformSubmissionInterface $webform_submission) {
if (empty($conditions)) {
return TRUE;
}
if (WebformArrayHelper::isSequential($conditions)) {
$condition_logic = in_array('xor', $conditions) ? 'xor' : 'or';
}
else {
$condition_logic = 'and';
}
$condition_results = [];
foreach ($conditions as $index => $value) {
if (is_string($value) && in_array($value, [
'and',
'or',
'xor',
])) {
continue;
}
if (is_int($index) && is_array($value)) {
$nested_result = $this
->validateConditions($value, $webform_submission);
if ($nested_result === NULL) {
return NULL;
}
$condition_results[] = $nested_result;
}
else {
if (is_int($index)) {
$selector = key($value);
$condition = $value[$selector];
}
else {
$selector = $index;
$condition = $value;
}
$condition_result = $this
->validateCondition($selector, $condition, $webform_submission);
if ($condition_result === NULL) {
return NULL;
}
$condition_results[] = $this
->validateCondition($selector, $condition, $webform_submission);
}
}
$conditions_sum = array_sum($condition_results);
$conditions_total = count($condition_results);
switch ($condition_logic) {
case 'xor':
return $conditions_sum === 1;
case 'or':
return (bool) $conditions_sum;
case 'and':
return $conditions_sum === $conditions_total;
default:
return NULL;
}
}
protected function validateCondition($selector, array $condition, WebformSubmissionInterface $webform_submission) {
$input_name = static::getSelectorInputName($selector);
if (!$input_name) {
return NULL;
}
$element_key = static::getInputNameAsArray($input_name, 0);
$element = $webform_submission
->getWebform()
->getElement($element_key);
if (!$element && strpos($selector, ':input[name="files[') !== FALSE) {
$element_key = static::getInputNameAsArray($input_name, 1);
$element = $webform_submission
->getWebform()
->getElement($element_key);
}
if (!$element) {
return NULL;
}
if (WebformArrayHelper::isSequential($condition)) {
$sub_condition_results = [];
foreach ($condition as $sub_condition) {
$sub_condition_results[] = $this
->checkCondition($element, $selector, $sub_condition, $webform_submission);
}
return (bool) array_sum($sub_condition_results);
}
else {
return $this
->checkCondition($element, $selector, $condition, $webform_submission);
}
}
protected function checkCondition(array $element, $selector, array $condition, WebformSubmissionInterface $webform_submission) {
$trigger = key($condition);
$trigger_value = $condition[$trigger];
$element_plugin = $this->elementManager
->getElementInstance($element);
if ($element_plugin instanceof WebformElement) {
return TRUE;
}
$element_value = $element_plugin
->getElementSelectorInputValue($selector, $trigger, $element, $webform_submission);
if ($trigger === 'value' && is_array($trigger_value)) {
$trigger_substate = key($trigger_value);
if (in_array($trigger_substate, [
'pattern',
'!pattern',
'less',
'less_equal',
'greater',
'greater_equal',
'between',
'!between',
])) {
$trigger = $trigger_substate;
$trigger_value = reset($trigger_value);
}
}
list($trigger, $trigger_negate) = $this
->processState($trigger);
if ($element_plugin
->hasMultipleValues($element) && $trigger !== 'empty') {
$result = FALSE;
$element_values = (array) $element_value;
foreach ($element_values as $element_value) {
$trigger_result = $this
->checkConditionTrigger($trigger, $trigger_value, $element_value);
if ($trigger_result !== FALSE) {
$result = $trigger_result;
}
}
}
else {
$result = $this
->checkConditionTrigger($trigger, $trigger_value, $element_value);
}
if ($result === NULL) {
return FALSE;
}
return $trigger_negate ? !$result : $result;
}
protected function checkConditionTrigger($trigger, $trigger_value, $element_value) {
switch ($trigger) {
case 'empty':
$empty = empty($element_value) && $element_value !== '0';
return $empty === (bool) $trigger_value;
case 'checked':
return (bool) $element_value === (bool) $trigger_value;
case 'value':
return (string) $element_value === (string) $trigger_value;
case 'pattern':
$pcre_pattern = preg_replace('/\\\\u([a-fA-F0-9]{4})/', '\\x{\\1}', $trigger_value);
return preg_match('{' . $pcre_pattern . '}u', $element_value);
case 'less':
return $element_value !== '' && floatval($trigger_value) > floatval($element_value);
case 'less_equal':
return $element_value !== '' && floatval($trigger_value) >= floatval($element_value);
case 'greater':
return $element_value !== '' && floatval($trigger_value) < floatval($element_value);
case 'greater_equal':
return $element_value !== '' && floatval($trigger_value) <= floatval($element_value);
case 'between':
if ($element_value === '') {
return NULL;
}
$greater = NULL;
$less = NULL;
if (strpos($trigger_value, ':') === FALSE) {
$greater = $trigger_value;
}
else {
list($greater, $less) = explode(':', $trigger_value);
}
$is_greater_than = $greater === NULL || $greater === '' || floatval($element_value) >= floatval($greater);
$is_less_than = $less === NULL || $less === '' || floatval($element_value) <= floatval($less);
return $is_greater_than && $is_less_than;
default:
return NULL;
}
}
protected function processState($state) {
if (isset($this->aliases[$state])) {
$state = $this->aliases[$state];
}
$negate = FALSE;
if (strpos($state, '!') === 0) {
$negate = TRUE;
$state = ltrim($state, '!');
}
return [
$state,
$negate,
];
}
protected function &getBuildElements(array &$form) {
if (isset($form['#webform_id']) && isset($form['elements'])) {
$form_elements =& $form['elements'];
}
else {
$form_elements =& $form;
}
$elements = [];
$this
->getBuildElementsRecursive($elements, $form_elements);
return $elements;
}
protected function getBuildElementsRecursive(array &$elements, array &$form, array $parent_states = []) {
foreach ($form as $key => &$element) {
if (!WebformElementHelper::isElement($element, $key)) {
continue;
}
$subelement_states = $parent_states;
if (!empty($element['#states']) || !empty($parent_states)) {
if (!empty($element['#required'])) {
if (!isset($element['#states']['required']) && !isset($element['#states']['optional'])) {
if (isset($element['#states']['visible'])) {
$element['#states']['required'] = $element['#states']['visible'];
}
elseif (isset($element['#states']['visible-slide'])) {
$element['#states']['required'] = $element['#states']['visible-slide'];
}
elseif (isset($element['#states']['invisible'])) {
$element['#states']['optional'] = $element['#states']['invisible'];
}
elseif (isset($element['#states']['invisible-slide'])) {
$element['#states']['optional'] = $element['#states']['invisible-slide'];
}
elseif ($parent_states) {
$element += [
'#states' => [],
];
$element['#states'] += $parent_states;
}
}
if (isset($element['#states']['optional']) || isset($element['#states']['required'])) {
unset($element['#required']);
}
if (!isset($element['#required'])) {
$element['#_required'] = TRUE;
}
}
if (isset($element['#states']['visible'])) {
$subelement_states = [
'required' => $element['#states']['visible'],
];
}
elseif (isset($element['#states']['visible-slide'])) {
$subelement_states = [
'required' => $element['#states']['visible-slide'],
];
}
elseif (isset($element['#states']['invisible'])) {
$subelement_states = [
'optional' => $element['#states']['invisible'],
];
}
elseif (isset($element['#states']['invisible-slide'])) {
$subelement_states = [
'optional' => $element['#states']['invisible-slide'],
];
}
}
if (isset($element['#access'])) {
$element['#_webform_access'] = $element['#access'];
}
if (!WebformElementHelper::isAccessibleElement($element)) {
continue;
}
$elements[$key] =& $element;
$this
->getBuildElementsRecursive($elements, $element, $subelement_states);
$element_plugin = $this->elementManager
->getElementInstance($element);
if ($element_plugin instanceof WebformCompositeBase && !$element_plugin
->hasMultipleValues($element)) {
if ($subelement_states) {
$composite_elements = $element_plugin
->getCompositeElements();
foreach ($composite_elements as $composite_key => $composite_element) {
if (isset($element['#' . $composite_key . '__access']) && $element['#' . $composite_key . '__access'] === FALSE) {
continue;
}
if (!empty($element['#' . $composite_key . '__required'])) {
unset($element['#' . $composite_key . '__required']);
$element['#' . $composite_key . '___required'] = TRUE;
$element['#' . $composite_key . '__states'] = $subelement_states;
}
}
}
}
elseif (isset($element['#element']) && isset($element['#webform_composite_elements'])) {
$this
->getBuildElementsRecursive($elements, $element['#element'], $subelement_states);
}
}
}
protected function getConditionTargetsVisibility(array $conditions, array $elements) {
$targets = [];
$this
->getConditionTargetsVisibilityRecursive($conditions, $targets);
foreach ($targets as $selector) {
$input_name = static::getSelectorInputName($selector);
if (!$input_name) {
$targets[$selector] = FALSE;
continue;
}
$element_key = static::getInputNameAsArray($input_name, 0);
if (!isset($elements[$element_key])) {
$targets[$selector] = FALSE;
continue;
}
$targets[$selector] = TRUE;
}
return $targets;
}
protected function getConditionTargetsVisibilityRecursive(array $conditions, array &$targets = []) {
foreach ($conditions as $index => $value) {
if (is_int($index) && is_array($value) && WebformArrayHelper::isSequential($value)) {
$this
->getConditionTargetsVisibilityRecursive($value, $targets);
}
elseif (is_string($value) && in_array($value, [
'and',
'or',
'xor',
])) {
continue;
}
elseif (is_int($index)) {
$selector = key($value);
$targets[$selector] = $selector;
}
else {
$selector = $index;
$targets[$selector] = $selector;
}
}
}
protected function addStatesHiddenToElement(array &$element) {
$element_plugin = $this->elementManager
->getElementInstance($element);
$attributes_property = $element_plugin
->hasWrapper($element) || $element_plugin
->getPluginDefinition()['states_wrapper'] ? '#wrapper_attributes' : '#attributes';
$element += [
$attributes_property => [],
];
$element[$attributes_property] += [
'class' => [],
];
$element[$attributes_property]['class'][] = 'js-webform-states-hidden';
}
public static function getSelectorInputName($selector) {
return preg_match('/\\:input\\[name="([^"]+)"\\]/', $selector, $match) ? $match[1] : NULL;
}
public static function getInputNameAsArray($name, $index = NULL) {
$name = str_replace([
'][',
'[',
']',
], [
'|',
'|',
'',
], $name);
$array = explode('|', $name);
if ($index !== NULL) {
return isset($array[$index]) ? $array[$index] : NULL;
}
else {
return $array;
}
}
}