Skip to content
webform_validation.validators.inc 41.1 KiB
Newer Older
Liam Morland's avatar
Liam Morland committed
 * Provides validation functionality and hooks.
 * Implements hook_webform_validation_validators().
Liam Morland's avatar
Liam Morland committed
 * This function returns an array of validators, in the validator key => options
 * array form. Possible options:
 * - name (required): name of the validator.
Liam Morland's avatar
Liam Morland committed
 * - component_types (required): defines which component types can be validated
Liam Morland's avatar
Liam Morland committed
 *   by this validator. Specify 'all' to allow all types.
 * - negatable (optional): define whether the rule can be negated, meaning it
 *   will validate the inverse of the rule.
 * - custom_error (optional): define whether a user can specify a custom error
 *   message upon creating the validation rule.
 * - custom_data (optional): define whether custom data can be added to the
 *   validation rule.
 * - min_components (optional): define the minimum number of components to be
 *   selected for creating a validation rule.
 * - max_components (optional): define the maximum number of components to be
 *   selected for creating a validation rule.
 * - description (optional): provide a descriptive explanation about the
 *   validator.
 */
function webform_validation_webform_validation_validators() {
    'numeric' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Numeric values'),
      'component_types' => array(
Daniel Imhoff's avatar
Daniel Imhoff committed
        'number',
        'label' => t('Numeric validation range'),
        'description' => t('Optionally specify the minimum-maximum range to validate the user-entered numeric value against. Usage:') . theme('table', array(
          'header' => array(t('Value'), t('Meaning')),
          'rows' => array(
            array(t('(empty)'), t('No value validation')),
            array('100', t('Greater than or equal to 100')),
            array('|100', t('Less than or equal to 100 (including negative numbers)')),
            array('0|100', t('Greater than or equal to 0 & less than or equal to 100')),
            array('10|100', t('Greater than or equal to 10 & less than or equal to 100')),
            array('-100|-10', t('Greater than or equal to -100 & less than or equal to -10')),
          ),
        )),
        'validate_regex' => '/^((-?[\d.]+)?\|)?(-?[\d.]+)$/',
      ),
      'description' => t('Verifies that user-entered values are numeric, with the option to specify min and / or max values.'),
    ),
    'min_length' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Minimum length'),
      'component_types' => array(
        'email',
        'number',
        'textarea',
        'textfield',
      ),
      'custom_data' => array(
        'label' => t('Minimum number of characters'),
        'description' => t('Specify the minimum number of characters that have to be entered to pass validation.'),
      'description' => t('Verifies that a user-entered value contains at least the specified number of characters.'),
    ),
    'max_length' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Maximum length'),
      'component_types' => array(
        'email',
        'number',
        'textarea',
        'textfield',
      ),
      'custom_data' => array(
        'label' => t('Maximum number of characters'),
        'description' => t('Specify the maximum number of characters that can be entered to pass validation.'),
      'description' => t('Verifies that a user-entered value contains at most the specified number of characters.'),
    'min_words' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Minimum number of words'),
      'component_types' => array(
        'hidden',
      ),
      'custom_data' => array(
        'label' => t('Minimum number of words'),
Liam Morland's avatar
Liam Morland committed
        'description' => t('Specify the minimum number of words that have to be entered to pass validation. Words are defined as strings of letters separated by spaces.'),
Liam Morland's avatar
Liam Morland committed
      'description' => t('Verifies that a user-entered value contains at least the specified number of words.'),
    ),
    'max_words' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Maximum number of words'),
      'component_types' => array(
        'hidden',
      ),
      'custom_data' => array(
        'label' => t('Maximum number of words'),
Liam Morland's avatar
Liam Morland committed
        'description' => t('Specify the maximum number of words that have to be entered to pass validation. Words are defined as strings of letters separated by spaces.'),
Liam Morland's avatar
Liam Morland committed
      'description' => t('Verifies that a user-entered value contains at most the specified number of words.'),
    // Only available in Webform 4; removed below if not.
    'sum' => array(
      'name' => t('Adds up to'),
      'component_types' => array(
        'number',
      ),
      'custom_data' => array(
        'label' => t('Number the fields add up to'),
        'description' => t('Specify the number and the type of comparison. For example:') . theme('table', array(
          'header' => array(t('Value'), t('Meaning')),
          'rows' => array(
            array('=3', t('The components must add up to exactly 3.')),
            array('>10', t('The components must add up to greater than 10.')),
            array('>=10', t('The components must add up to greater than or equal to 10.')),
            array('<20', t('The components must add up to less than 20.')),
            array('<=20', t('The components must add up to less than or equal to 20.')),
Liam Morland's avatar
Liam Morland committed
          ),
        )),
        'validate_regex' => '/^(=|>|>=|<|<=)(-?[\d.]+)$/',
      ),
      'description' => t('Require the values of the selected fields to add up to exactly, greater than or equal to, or less than or equal to a specified number.'),
    ),
    'equal' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Equal values'),
      'component_types' => array(
      ),
      'min_components' => 2,
      'description' => t('Verifies that all specified components contain equal values. If all components are of type email, they will get case-insensitive comparison.'),
Liam Morland's avatar
Liam Morland committed
    'comparison' => array(
      'name' => t('Compare two values'),
      'component_types' => array(
        'date',
        'email',
        'hidden',
        'number',
        'select',
Liam Morland's avatar
Liam Morland committed
        'time',
      ),
      'custom_error' => TRUE,
      'custom_data' => array(
        'label' => t('Comparison operator'),
        'description' => t('Specify the comparison operator you want to use. Must be one of: >, >=, <, <=. The validator will compare the first component with the second using this operator. If the components are of type email, they will get case-insensitive comparison.'),
Liam Morland's avatar
Liam Morland committed
      ),
      'min_components' => 2,
      'max_components' => 2,
      'description' => t('Compare two values for greater than (>), less than (<), greater than or equal to (>=), or less than or equal to (<=).'),
Liam Morland's avatar
Liam Morland committed
    ),
    'unique' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Unique values'),
      'component_types' => array(
      ),
      'min_components' => 2,
      'description' => t('Verifies that none of the specified components contain the same value as another selected component in this submission. (To check that values are unique between submissions, use the unique validation option on the "Edit component" page for that component.) If all components are of type email, they will get case-insensitive comparison.'),
Liam Morland's avatar
Liam Morland committed
      'name' => t('Specific value(s)'),
Daniel Imhoff's avatar
Daniel Imhoff committed
        'number',
      ),
      'custom_error' => TRUE,
      'custom_data' => array(
        'label' => t('(Key) value'),
        'description' => t('Specify the specific value(s) you want the component to contain. Separate multiple options by a comma. For components that have keys, use the key value instead.'),
      'description' => t('Verifies that the value of the specified component is from a list of allowed values.'),
    'default_value' => array(
      'name' => t('Default value'),
      'negatable' => TRUE,
      'component_types' => array(
        'email',
        'hidden',
        'number',
        'select',
        'textarea',
        'textfield',
      'description' => t('Verifies that the user-entered value is the default value for that component. Negate if you want not the default value.'),
    'someofseveral' => array(
      'component_types' => array(
Liam Morland's avatar
Liam Morland committed
        'time',
        'description' => t('Specify the number that must be completed and the type of comparison. For example:') . theme('table', array(
          'header' => array(t('Value'), t('Meaning')),
          'rows' => array(
            array('>=1', t('The user must complete <b>at least</b> 1 of the selected components.')),
            array('=3', t('The user must complete <b>exactly</b> 3 of the selected components.')),
            array('<=2', t('The user must complete <b>at most</b> 2 of the selected components.')),
Liam Morland's avatar
Liam Morland committed
          ),
        )),
      'min_components' => 2,
      'description' => t('Requires the user to complete some number of components out of a group of components. For example, complete at least 2 out of 3, complete at most 4 out of 6, or complete exactly 3 our of 4.'),
Liam Morland's avatar
Liam Morland committed
      'name' => t('Minimum number of selections required'),
      'component_types' => array(
        'select',
      ),
      'custom_data' => array(
        'label' => t('Minimum number of selections'),
        'description' => t('Specify the minimum number of options a user should select.'),
Liam Morland's avatar
Liam Morland committed
      'description' => t('Forces the user to select at least a defined number of options from the specified webform components.'),
Liam Morland's avatar
Liam Morland committed
      'name' => t('Maximum number of selections allowed'),
      'component_types' => array(
        'select',
      ),
      'custom_data' => array(
        'label' => t('Maximum number of selections'),
        'description' => t('Specify the maximum number of options a user can select.'),
Liam Morland's avatar
Liam Morland committed
      'description' => t('Forces the user to select at most a defined number of options from the specified webform components.'),
Liam Morland's avatar
Liam Morland committed
      'name' => t('Exact number of selections required'),
      'component_types' => array(
        'select',
      ),
      'custom_data' => array(
        'label' => t('Number of selections'),
        'description' => t('Specify how many options a user can select.'),
Liam Morland's avatar
Liam Morland committed
      'description' => t('Forces the user to select exactly the defined number of options from the specified webform components.'),
    'plain_text' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Plain text (disallow tags)'),
      'component_types' => array(
        'email',
Liam Morland's avatar
Liam Morland committed
      'description' => t("Verifies that user-entered data doesn't contain HTML tags."),
    'starts_with' => array(
      'name' => t('Starts with'),
      'component_types' => array(
        'email',
        'hidden',
        'number',
        'textarea',
        'textfield',
      ),
      'custom_data' => array(
        'label' => t('Starts with'),
        'description' => t('Enter the text that this field must start with.'),
      ),
      'description' => t('Verifies that user-entered data starts with a given string.'),
    ),
    'ends_with' => array(
      'name' => t('Ends with'),
      'component_types' => array(
        'email',
        'hidden',
        'number',
        'textarea',
        'textfield',
      ),
      'custom_data' => array(
        'label' => t('Ends with'),
        'description' => t('Enter the text that this field must end with.'),
      ),
      'description' => t('Verifies that user-entered data ends with a given string.'),
    ),
    'pattern' => array(
      'name' => t('Pattern'),
      'component_types' => array(
        'hidden',
      ),
      'custom_error' => TRUE,
      'custom_data' => array(
        'label' => t('Pattern'),
        'description' => t('Specify a pattern where:') . theme('table', array(
          'header' => array(t('Value'), t('Meaning')),
          'rows' => array(
            array('@', t('Any letter A-Z.')),
            array('#', t('Any numeral 0-9.')),
            array('|', t('Separates two or more acceptable patterns.')),
            array('', t('Any other character must appear in its exact position.')),
          ),
        )) . t('Examples:') . theme('table', array(
          'header' => array(t('Value'), t('Meaning')),
          'rows' => array(
            array('(###) ###-####', t('North American phone number.')),
            array('D-25##', t('D-2500 series model numbers.')),
            array('@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA', t('UK Postal Code.')),
          ),
        )),
      ),
      'description' => t('Verifies that a user-entered value follows to a specified pattern.'),
    ),
    'regex' => array(
      'name' => t('Regular expression, case-sensitive'),
      'component_types' => array(
        'email',
        'number',
        'textarea',
        'textfield',
      ),
      'custom_error' => TRUE,
      'custom_data' => array(
        'label' => t('Regex code (case-sensitive)'),
        'description' => t('Specify regex code to validate the user input against. Do not include delimiters such as /.'),
      'description' => t('Validates user-entered text against a case-sensitive regular expression.'),
    ),
    'regexi' => array(
      'name' => t('Regular expression, case-insensitive'),
      'component_types' => array(
        'email',
        'hidden',
        'number',
        'textarea',
        'textfield',
      ),
      'custom_error' => TRUE,
      'custom_data' => array(
        'label' => t('Regex code (case-insensitive)'),
        'description' => t('Specify regex code to validate the user input against. Do not include delimiters such as /.'),
      'description' => t('Validates user-entered text against a case-insensitive regular expression.'),
    'must_be_empty' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Must be empty'),
      'component_types' => array(
Daniel Imhoff's avatar
Daniel Imhoff committed
        'number',
Liam Morland's avatar
Liam Morland committed
      'description' => t('Verifies that a specified textfield remains empty. Recommended use case: used as an anti-spam measure by hiding the element with CSS.'),
Liam Morland's avatar
Liam Morland committed
      'name' => t('Words blacklist'),
      'component_types' => array(
        'email',
      ),
      'custom_error' => TRUE,
      'custom_data' => array(
        'label' => t('Blacklisted words'),
        'description' => t('Specify illegal words, seperated by commas.'),
Liam Morland's avatar
Liam Morland committed
      'description' => t("Validates that user-entered data doesn't contain any of the specified illegal words."),
Liam Morland's avatar
Liam Morland committed
    'username' => array(
Liam Morland's avatar
Liam Morland committed
      'name' => t('Must match a username'),
      'component_types' => array(
      'description' => t('Validates that user-entered data matches a username.'),
    'valid_url' => array(
      'name' => t('Valid URL'),
      'component_types' => array(
        'hidden',
      'description' => t('Validates that the user-entered data is a valid URL.'),
  // Only available in Webform 4.
  module_load_include('inc', 'webform', 'components/number');
  if (!function_exists('webform_compare_floats')) {
    unset($validators['sum']);
  }

  if (module_exists('email_verify')) {
    $validators['email_verify'] = array(
      'name' => t('Email Verify'),
      'component_types' => array(
        'email',
      ),
      'description' => t('Verifies that an email address actually exists using the <a href="https://drupal.org/project/email_verify">Email Verification module</a>.'),
    );
  }

  if (module_exists('maxlength')) {
    $validators['max_length']['description'] .= ' ' . t('A JavaScript counter will show the number of characters remaining as the user types.');
  }

 * Implements hook_webform_validation_validate().
 */
function webform_validation_webform_validation_validate($validator_name, $items, $components, $rule) {

  // For certain validators, if all components to be compared are email
  // components, make the values lower-case to avoid case-sensitive comparison.
  if (in_array($validator_name, array('equal', 'comparison', 'unique'))) {
    $all_email = TRUE;
    foreach ($rule['components'] as $component) {
      if ($component['type'] !== 'email') {
        $all_email = FALSE;
        break;
      }
    }
    if ($all_email) {
      $items = array_map('strtolower', $items);
    }
  }

Liam Morland's avatar
Liam Morland committed
  // Some components, such as multiple select, send their values as arrays,
  // others don't. Convert all to arrays and write the rules to handle them that
  // way. Remove empty values.
  foreach ($items as $key => $val) {
    $new_values = array();
    foreach ((array) $val as $val_key => $val_val) {
      if ((string) $val_val !== '') {
        $new_values[$val_key] = $val_val;
    $items[$key] = $new_values;
  }
  // Ensure later calls to key() get the first item.
  reset($items);
  $errors = array();
  switch ($validator_name) {
    case 'numeric':
      $num_range = _webform_numeric_check_data($rule['data']);
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
Liam Morland's avatar
Liam Morland committed
          // First check if the value is numeric.
          if (is_numeric($subval)) {
            $subval = (float) $subval;
          }
          else {
            $errors[$key] = t('%item is not numeric.', array('%item' => $components[$key]['name']));
            continue;
          }
Liam Morland's avatar
Liam Morland committed
          // Now validate the entered numeric value against the validator range
          // settings, if appropriate.
Liam Morland's avatar
Liam Morland committed
          // a. validate min & max.
          if (isset($num_range['min']) && isset($num_range['max'])) {
Liam Morland's avatar
Liam Morland committed
            // Validate the min - max range.
            if ($subval < $num_range['min'] || $subval > $num_range['max']) {
Liam Morland's avatar
Liam Morland committed
              $options = array(
                '%item' => $components[$key]['name'],
                '%range' => str_replace('|', ' - ', $rule['data']),
              );
              $errors[$key] = t('%item is not within the allowed range %range.', $options);
Liam Morland's avatar
Liam Morland committed
            // b. validate min.
            if (isset($num_range['min'])) {
              if ($subval < $num_range['min']) {
                $errors[$key] = t('%item should be greater than or equal to %val.', array('%item' => $components[$key]['name'], '%val' => $num_range['min']));
Liam Morland's avatar
Liam Morland committed
            // c. validate max.
            if (isset($num_range['max'])) {
              if ($subval > $num_range['max']) {
                $errors[$key] = t('%item should be less than or equal to %val.', array('%item' => $components[$key]['name'], '%val' => $num_range['max']));
Liam Morland's avatar
Liam Morland committed

    case 'min_length':
      $min_length = $rule['data'];
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          if (drupal_strlen($subval) < $min_length) {
            $errors[$key] = t('%item should be at least %num characters long.', array('%item' => $components[$key]['name'], '%num' => $min_length));
Liam Morland's avatar
Liam Morland committed

    case 'max_length':
      $max_length = $rule['data'];
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          if (drupal_strlen($subval) > $max_length) {
            $errors[$key] = t('%item should be at most %num characters long.', array('%item' => $components[$key]['name'], '%num' => $max_length));
Liam Morland's avatar
Liam Morland committed

    case 'min_words':
      $min_words = $rule['data'];
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          if (_webform_validation_count_words($subval) < $min_words) {
            $error = format_plural($min_words, '%item should be at least 1 word long.', '%item should be at least @count words long.', array('%item' => $components[$key]['name']));
            $errors[$key] = $error;
Liam Morland's avatar
Liam Morland committed
          }
        }
Liam Morland's avatar
Liam Morland committed

    case 'max_words':
      $max_words = $rule['data'];
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          if (_webform_validation_count_words($subval) > $max_words) {
            $error = format_plural($max_words, '%item should be at most 1 word long.', '%item should be at most @count words long.', array('%item' => $components[$key]['name']));
            $errors[$key] = $error;
Liam Morland's avatar
Liam Morland committed
          }
        }
Liam Morland's avatar
Liam Morland committed

    case 'sum':
      // Get the numbers to sum.
      $sum = array();
      foreach ($items as $item) {
        if (isset($item[0])) {
          $sum[] = $item[0];
        }
      }
      // If none of the components are completed, don't run this validator.
      if (!count($sum)) {
      }
      $sum = array_sum($sum);

      // Number being compared with.
      $compare_number = (float) preg_replace('/^[^0-9]+/', '', $rule['data']);

      // Parse the comparision operator and do comparison.
      module_load_include('inc', 'webform', 'components/number');
      $error = FALSE;
      if (substr($rule['data'], 0, 2) === '<=') {
        if (!(webform_compare_floats($sum, $compare_number) <= 0)) {
          $error = t('less than or equal to');
        }
      }
      elseif (substr($rule['data'], 0, 1) === '<') {
        if (!(webform_compare_floats($sum, $compare_number) < 0)) {
          $error = t('less than');
        }
      }
      elseif (substr($rule['data'], 0, 2) === '>=') {
        if (!(webform_compare_floats($sum, $compare_number) >= 0)) {
          $error = t('greater than or equal to');
        }
      }
      elseif (substr($rule['data'], 0, 1) === '>') {
        if (!(webform_compare_floats($sum, $compare_number) > 0)) {
        if (!(webform_compare_floats($sum, $compare_number) === 0)) {
          $error = t('exactly');
        }
      }
      // Set error if needed.
      if ($error) {
        $keys = array_keys($items);
        $errors[$keys[0]] = t('These items must add up to %verb %compare_number:', array('%verb' => $error, '%compare_number' => $compare_number)) . theme('item_list', array('items' => _webform_validation_get_names_of_rule_components($rule)));
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'equal':
      $first_entry_key = key($items);
      $first_entry = array_shift($items);
      foreach ($items as $key => $val) {
        if ($val !== $first_entry) {
          $errors[$key] = t('%item_checked does not match %item_first.', array('%item_checked' => $components[$key]['name'], '%item_first' => $components[$first_entry_key]['name']));
        }
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'comparison':
      foreach (array(1, 2) as $count) {
        $entry[$count]['key'] = key($items);
        $value = array_shift($items);
        if ($components[$entry[$count]['key']]['type'] === 'date') {
          if (!empty($value) && checkdate((int) $value['month'], (int) $value['day'], (int) $value['year'])) {
            $entry[$count]['value'] = date('Y-m-d', mktime(0, 0, 0, (int) $value['month'], (int) $value['day'], (int) $value['year']));
          }
          else {
            $entry[$count]['value'] = NULL;
        elseif ($components[$entry[$count]['key']]['type'] === 'time') {
          if (isset($value['hour']) && isset($value['minute'])) {
            $time = $value['hour'] . ':' . str_pad($value['minute'], 2, '0', STR_PAD_LEFT);
            if (!empty($value['ampm'])) {
              $time .= ' ' . $value['ampm'];
            }
            $time = strtotime($time);
          }
          else {
            $time = NULL;
Liam Morland's avatar
Liam Morland committed
          }
          if ($time) {
            $entry[$count]['value'] = date('H:i', $time);
Liam Morland's avatar
Liam Morland committed
          }
          else {
            $entry[$count]['value'] = NULL;
Liam Morland's avatar
Liam Morland committed
          }
        }
        else {
          $entry[$count]['value'] = _webform_validation_flatten_array($value);
Liam Morland's avatar
Liam Morland committed
        }
Liam Morland's avatar
Liam Morland committed

      // Don't validate if either are empty.
      if ((string) $entry[1]['value'] === '' || (string) $entry[2]['value'] === '') {
Liam Morland's avatar
Liam Morland committed

      $error = FALSE;
      switch ($rule['data']) {
        case '>':
          if (!($entry[1]['value'] > $entry[2]['value'])) {
            $error = TRUE;
Liam Morland's avatar
Liam Morland committed

        case '>=':
          if (!($entry[1]['value'] >= $entry[2]['value'])) {
            $error = TRUE;
Liam Morland's avatar
Liam Morland committed

        case '<':
          if (!($entry[1]['value'] < $entry[2]['value'])) {
            $error = TRUE;
Liam Morland's avatar
Liam Morland committed

        case '<=':
          if (!($entry[1]['value'] <= $entry[2]['value'])) {
            $error = TRUE;
          break;
      }

      if ($error) {
        $errors[$entry[2]['key']] = _webform_validation_i18n_error_message($rule);
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'unique':
      foreach ($items as $key => $val) {
        $items[$key] = _webform_validation_flatten_array($val);
      }
Liam Morland's avatar
Liam Morland committed
      // Now we count how many times each value appears, and find out which
      // values appear more than once.
      $items_count = array_count_values(array_map('drupal_strtolower', array_map('trim', $items)));
      $doubles = array_filter($items_count, function ($x) {
        return $x > 1;
      });
      foreach ($items as $key => $val) {
        if (in_array(drupal_strtolower($val), array_keys($doubles))) {
          $errors[$key] = t('The value of %item is not unique.', array('%item' => $components[$key]['name']));
Liam Morland's avatar
Liam Morland committed

    case 'specific_value':
      $specific_values = explode(',', $rule['data']);
      $specific_values = array_map('trim', $specific_values);
      foreach ($items as $key => $val) {
Liam Morland's avatar
Liam Morland committed
        // Selects: Fail if at least one checked and at least one not in the
        // allowed values.
        $val = array_filter($val);
        $test = count($val) && count(array_diff($val, $specific_values));
        _webform_validation_test($errors, $key, $rule, $test);
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'default_value':
      foreach ($items as $key => $val) {
        $val = _webform_validation_flatten_array($val);
        $test = $val != $components[$key]['value'];
        _webform_validation_test($errors, $key, $rule, $test);
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'someofseveral':
      // Determine the number of components completed.
      foreach ($items as $key => $val) {
        $items[$key] = _webform_validation_flatten_array($val);
        $items[$key] = (bool) strlen($items[$key]);
      }
      $number_completed = count(array_filter($items));

      // Number being compared with.
      $compare_number = (int) preg_replace('/[^0-9]+/', '', $rule['data']);
      if ($compare_number < 1) {
        $compare_number = 1;
      }
      elseif ($compare_number > count($rule['components'])) {
        $compare_number = count($rule['components']);
      }

      // Parse the comparision operator and do comparison.
      $error = FALSE;
      if (substr($rule['data'], 0, 1) === '=') {
        if (!($number_completed === $compare_number)) {
          $error = t('exactly');
        }
      }
      elseif (substr($rule['data'], 0, 2) === '<=') {
        if (!($number_completed <= $compare_number)) {
          $error = t('at most');
        }
      }
      else {
        if (!($number_completed >= $compare_number)) {
          $error = t('at least');
        }
      }

      // Set error if needed.
      if ($error) {
        $keys = array_keys($items);
        $errors[$keys[0]] = t('You must complete %verb %compare_number of these items:', array('%verb' => $error, '%compare_number' => $compare_number)) . theme('item_list', array('items' => _webform_validation_get_names_of_rule_components($rule)));
Liam Morland's avatar
Liam Morland committed

    case 'select_min':
      $min_selections = $rule['data'];
      foreach ($items as $key => $val) {
        if (count(array_filter($val, '_webform_validation_check_false')) < $min_selections) {
          $errors[$key] = t('Please select at least %num options for %item.', array('%num' => $min_selections, '%item' => $components[$key]['name']));
Liam Morland's avatar
Liam Morland committed

    case 'select_max':
      $max_selections = $rule['data'];
      foreach ($items as $key => $val) {
        if (count(array_filter($val, '_webform_validation_check_false')) > $max_selections) {
          $errors[$key] = t('Please select maximum %num options for %item.', array('%num' => $max_selections, '%item' => $components[$key]['name']));
Liam Morland's avatar
Liam Morland committed

    case 'select_exact':
      $allowed_selections = $rule['data'];
      foreach ($items as $key => $val) {
        $error_strings = array(
          'regular' => 'Please select %num options for %item.',
          'negated' => 'Please do not select %num options for %item.',
        );
        $error_vars = array('%num' => $allowed_selections, '%item' => $components[$key]['name']);
        $test = count(array_filter($val, '_webform_validation_check_false')) != $allowed_selections;
        _webform_validation_test($errors, $key, $rule, $test, $error_strings, $error_vars);
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'plain_text':
      foreach ($items as $key => $val) {
        $error_strings = array(
          'regular' => '%item only allows the use of plain text.',
          'negated' => '%item must contain HTML tags.',
        );
        $error_vars = array('%item' => $components[$key]['name']);
        foreach ($val as $subval) {
          $test = strcmp($subval, strip_tags($subval));
          _webform_validation_test($errors, $key, $rule, $test, $error_strings, $error_vars);
Liam Morland's avatar
Liam Morland committed

    case 'starts_with':
    case 'ends_with':
      if ($validator_name === 'starts_with') {
        $pattern = '^' . $pattern;
        $verb = t('start');
      }
      else {
        $pattern .= '$';
        $verb = t('end');
      }
      $pattern = '/' . $pattern . '/';
      foreach ($items as $key => $val) {
        $error_strings = array(
          'regular' => '%item must %verb with "%string".',
          'negated' => '%item must not %verb with "%string".',
        );
Liam Morland's avatar
Liam Morland committed
        $error_vars = array(
          '%item' => $components[$key]['name'],
          '%verb' => $verb,
          '%string' => $rule['data'],
        );
        foreach ($val as $subval) {
          $test = !preg_match($pattern, $subval);
          _webform_validation_test($errors, $key, $rule, $test, $error_strings, $error_vars);
Liam Morland's avatar
Liam Morland committed

      $pattern = str_replace('@', '[a-zA-Z]', $pattern);
      // Since PHP 7.3, the # character is quoted by preg_quote().
      $quoted_hash = version_compare(PHP_VERSION, '7.3.0', '>=') ? '\\#' : '#';
      $pattern = str_replace($quoted_hash, '[0-9]', $pattern);
      $pattern = '(' . $pattern . ')';
      // Un-escape "|" operator.
      $pattern = preg_replace('/(\\s*)(\\\\)(\\|)(\\s*)/', ')|(', $pattern);
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          $test = !preg_match('/^(' . $pattern . ')$/', $subval);
          _webform_validation_test($errors, $key, $rule, $test);
Liam Morland's avatar
Liam Morland committed

    case 'regex':
    case 'regexi':
      mb_regex_encoding('UTF-8');
      $regex = $rule['data'];
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          if ($validator_name === 'regexi') {
            $match = (bool) mb_eregi($regex, $subval);
          else {
            $match = (bool) mb_ereg($regex, $subval);
          $test = !$match;
          _webform_validation_test($errors, $key, $rule, $test);
Liam Morland's avatar
Liam Morland committed

    case 'must_be_empty':
      foreach ($items as $key => $val) {
        if (count($val) !== 0) {
          $errors[$key] = t('%item does not contain the correct data.', array('%item' => $components[$key]['name']));
Liam Morland's avatar
Liam Morland committed

    case 'blacklist':
      $blacklist = explode(',', $blacklist);
      $blacklist = array_map('trim', $blacklist);
      $blacklist = '/\b(' . implode('|', $blacklist) . ')\b/i';
      foreach ($items as $key => $val) {
        foreach ($val as $subval) {
          $test = preg_match($blacklist, $subval);
          _webform_validation_test($errors, $key, $rule, $test);
        }
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'username':
      foreach ($items as $key => $val) {
        $error_strings = array(
          'regular' => 'The %item field does not match an active username.',
          'negated' => 'The %item field matches an active username.',
        );
        $error_vars = array('%item' => $components[$key]['name']);
        foreach ($val as $subval) {
          $test = !db_query_range('SELECT 1 FROM {users} WHERE name = :username AND status = 1', 0, 1, array(':username' => $subval))->fetchField();
          _webform_validation_test($errors, $key, $rule, $test, $error_strings, $error_vars);
        }
      }
      return $errors;
Liam Morland's avatar
Liam Morland committed

    case 'valid_url':
      foreach ($items as $key => $val) {
        $error_strings = array(
          'regular' => '%item does not appear to be a valid URL.',
          'negated' => '%item must not be a valid URL.',
        );
        $error_vars = array('%item' => $components[$key]['name']);
        foreach ($val as $subval) {
          $test = !valid_url($subval, TRUE);
          _webform_validation_test($errors, $key, $rule, $test, $error_strings, $error_vars);
Liam Morland's avatar
Liam Morland committed

    case 'email_verify':
      if (module_exists('email_verify')) {
        foreach ($items as $key => $val) {
            $error = email_verify_check($subval);
            if ($error) {
              $errors[$key] = $error;
/**
 * Return an array of the names of the components in a validation rule.
 *
Liam Morland's avatar
Liam Morland committed
 * @param array $rule
 *   Array of information about a validation rule.
 *
 * @return array
 *   Array of the filtered names of the components in $rule.
 */
function _webform_validation_get_names_of_rule_components(array $rule) {
  $names = array();
  foreach ($rule['components'] as $component) {
    $names[] = _webform_filter_xss($component['name']);
  }