Commit db951489 authored by Ryan McVeigh's avatar Ryan McVeigh
Browse files

Issue #3270874 by rymcveigh: Validation bypass via multiple submit button clicks

parent a48775e2
Loading
Loading
Loading
Loading
+44 −16
Original line number Diff line number Diff line
@@ -2,10 +2,10 @@

namespace Drupal\webformnavigation\Plugin\WebformHandler;

use Drupal;
use Drupal\Core\Form\FormStateInterface;
use Drupal\webform\Plugin\WebformHandlerBase;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionForm;
use Drupal\webform\WebformSubmissionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

@@ -122,21 +122,18 @@ class WebformNavigationHandler extends WebformHandlerBase {
        // Allow user to access all but the confirmation page.
        if ($page_key != 'webform_confirmation') {
          $form['pages'][$page_key]['#access'] = TRUE;
          $form['pages'][$page_key]['#validate'] = $validations;
        }
      }
      // Set our loggers to the draft update if it is set.
      if (isset($form['actions']['draft'])) {
        // Add a logger to the next validators.
        $form['actions']['draft']['#validate'] = $validations;
          $form['pages'][$page_key]['#validate'] = [
            '::validateForm',
            '::draft',
          ];
          $form['pages'][$page_key]['#submit'] = [
            '::submitForm',
            '::save',
            '::rebuild',
          ];
          $form['pages'][$page_key]['#attributes']['formnovalidate'] = 'formnovalidate';
        }
      // Set our loggers to the previous update if it is set.
      if (isset($form['actions']['wizard_prev'])) {
        // Add a logger to the next validators.
        $form['actions']['wizard_prev']['#validate'] = $validations;
      }
      // Add a custom validator to the final submit.
      $form['actions']['submit']['#validate'][] = 'webformnavigation_submission_validation';
      // Log the page visit.
      $visited = $this->webformNavigationHelper->hasVisitedPage($webform_submission, $current_page);
      // Log the page if it has not been visited before.
@@ -149,11 +146,19 @@ class WebformNavigationHandler extends WebformHandlerBase {
        // Make sure we haven't already set errors.
        if (!empty($errors[$current_page])) {
          foreach ($errors[$current_page] as $error) {
            Drupal::messenger()->addError($error);
            \Drupal::messenger()->addError($error);
          }
        }
      }
    }
    // Bypass validation of the next click.
    $prevent_next_validation = $webform->getThirdPartySetting('webformnavigation', 'prevent_next_validation');

    // Actions to perform if prevent_next_validation is set.
    if ($prevent_next_validation && isset($form['actions']['wizard_next'])) {
      $form['actions']['wizard_next']['#validate'][] = '::noValidate';
      $form['actions']['wizard_next']['#validate'][] = '::draft';
    }
  }

  /**
@@ -168,7 +173,25 @@ class WebformNavigationHandler extends WebformHandlerBase {
    $forward_navigation = $webform->getThirdPartySetting('webformnavigation', 'forward_navigation');
    // Actions to perform if forward navigation is enabled and there are pages.
    if ($forward_navigation && $webform->hasWizardPages()) {
      $triggering_element = $form_state->getTriggeringElement();
      $this->webformNavigationHelper->logPageErrors($webform_submission, $form_state);
      if (isset($triggering_element['#page'])) {
        $form_state->clearErrors();
      }
      if (isset($triggering_element['#validate']) && in_array('::complete', $triggering_element['#validate'])) {
        $form_state->clearErrors();
        $logged_errors = $this->webformNavigationHelper->getErrors($webform_submission);
        foreach ($logged_errors as $page_name => $errors) {
          if (!empty($errors)) {
            $page = $webform->getPage('add', $page_name);
            $form_state->setErrorByName($page_name, [
              '#theme' => 'item_list',
              '#items' => $errors,
              '#title' => $page['#title'],
            ]);
          }
        }
      }
    }
  }

@@ -179,6 +202,11 @@ class WebformNavigationHandler extends WebformHandlerBase {
   */
  public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
    $this->debug(__FUNCTION__);
    $triggering_element = $form_state->getTriggeringElement();
    if (isset($triggering_element['#page'])) {
      $form_state->set('current_page', $triggering_element['#page']);
      $webform_submission->setCurrentPage($triggering_element['#page']);
    }
  }

  /**
@@ -245,7 +273,7 @@ class WebformNavigationHandler extends WebformHandlerBase {
    // Log the initial page if this is an insert.
    if (!$update && $forward_navigation && $webform->hasWizardPages()) {
      $pages = $webform->getPages('add', $webform_submission);
      // Log the first page
      // Log the first page.
      $this->webformNavigationHelper->logPageVisit($webform_submission, array_keys($pages)[0]);
      // Log any stashed errors.
      $this->webformNavigationHelper->logStashedPageErrors($webform_submission);
+27 −15
Original line number Diff line number Diff line
@@ -2,7 +2,6 @@

namespace Drupal\webformnavigation;

use Drupal;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
@@ -29,7 +28,7 @@ class WebformNavigationHelper {
  const ERROR_OPERATION = 'errors';

  /**
   * Name of the page visited operation
   * Name of the page visited operation.
   */
  const PAGE_VISITED_OPERATION = 'page visited';

@@ -44,6 +43,8 @@ class WebformNavigationHelper {
  const TEMP_STORE_KEY = 'webformnavigation_errors';

  /**
   * The webform submission log manager.
   *
   * @var \Drupal\webform_submission_log\WebformSubmissionLogManager
   */
  protected $webform_submission_log_manager;
@@ -80,10 +81,15 @@ class WebformNavigationHelper {
   * AutosaveHelper constructor.
   *
   * @param \Drupal\webform_submission_log\WebformSubmissionLogManager $webform_submission_log_manager
   *   The webform submission logger service.
   * @param \Drupal\Core\Database\Connection $datababse
   *   The database service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger 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(WebformSubmissionLogManager $webform_submission_log_manager, Connection $datababse, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder) {
    $this->webform_submission_log_manager = $webform_submission_log_manager;
@@ -142,14 +148,13 @@ class WebformNavigationHelper {
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission entity.
   *
   * @param string|null $page
   *   Set to page name if you only want the data for a particular page.
   *
   * @return array
   *   An array of errors.
   */
  public function getErrors(WebformSubmissionInterface $webform_submission, $page = NULL) {
  public function getErrors(WebformSubmissionInterface $webform_submission, string $page = NULL) {
    // Get outta here if the submission hasn't been saved yet.
    if (empty($webform_submission->id())) {
      return [];
@@ -196,9 +201,9 @@ class WebformNavigationHelper {
        'sid' => $webform_submission->id(),
        'operation' => self::PAGE_VISITED_OPERATION,
        'handler_id' => self::HANDLER_ID,
        'uid' => Drupal::currentUser()->id(),
        'uid' => \Drupal::currentUser()->id(),
        'data' => $page,
        'timestamp' => (string) Drupal::time()->getRequestTime(),
        'timestamp' => (string) \Drupal::time()->getRequestTime(),
      ];
      $query = $this->database->insert(self::TABLE, $fields);
      $query->fields($fields)->execute();
@@ -215,7 +220,7 @@ class WebformNavigationHelper {
   */
  public function logStashedPageErrors(WebformSubmissionInterface $webform_submission) {
    /** @var \Drupal\Core\TempStore\PrivateTempStore $store */
    $store = Drupal::service('tempstore.private')->get('webformnavigation');
    $store = \Drupal::service('tempstore.private')->get('webformnavigation');
    $errors = $store->get(self::TEMP_STORE_KEY);
    // Get outta here if there are not any stashed errors.
    if (empty($errors)) {
@@ -243,7 +248,9 @@ class WebformNavigationHelper {
    $form_errors = $form_state->getErrors();
    $current_errors = $this->getErrors($webform_submission);
    $paged_errors = empty($current_errors) ? [] : $current_errors;
    $current_page = $webform_submission->getCurrentPage();
    $current_page = $this->getCurrentPage($webform_submission);
    // Let's not create too many logs.
    $this->deleteSubmissionLogs($webform_submission, TRUE);
    // Reset the current page's errors with those set in the form state.
    $paged_errors[$current_page] = [];
    foreach ($form_errors as $element => $error) {
@@ -260,7 +267,7 @@ class WebformNavigationHelper {
    // Stash the errors and return if the submission hasn't been created yet.
    if (empty($webform_submission->id())) {
      /** @var \Drupal\Core\TempStore\PrivateTempStore $store */
      $store = Drupal::service('tempstore.private')->get('webformnavigation');
      $store = \Drupal::service('tempstore.private')->get('webformnavigation');
      $store->set(self::TEMP_STORE_KEY, $paged_errors);
      return;
    }
@@ -288,9 +295,9 @@ class WebformNavigationHelper {
        'sid' => $webform_submission->id(),
        'operation' => self::ERROR_OPERATION,
        'handler_id' => self::HANDLER_ID,
        'uid' => Drupal::currentUser()->id(),
        'uid' => \Drupal::currentUser()->id(),
        'data' => serialize($errors),
        'timestamp' => (string) Drupal::time()->getRequestTime(),
        'timestamp' => (string) \Drupal::time()->getRequestTime(),
      ];
      $this->database->insert(self::TABLE)->fields($fields)->execute();
    }
@@ -301,8 +308,10 @@ class WebformNavigationHelper {
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission entity.
   * @param bool $keep_visited
   *   Set to TRUE if you would like to keep the page visited logs.
   */
  public function deleteSubmissionLogs(WebformSubmissionInterface $webform_submission) {
  public function deleteSubmissionLogs(WebformSubmissionInterface $webform_submission, bool $keep_visited = FALSE) {
    // Get outta here if the submission hasn't been saved yet.
    if (empty($webform_submission->id())) {
      return;
@@ -310,6 +319,9 @@ class WebformNavigationHelper {
    $query = $this->database->delete(self::TABLE);
    $query->condition('webform_id', $webform_submission->getWebform()->id());
    $query->condition('sid', $webform_submission->id());
    if ($keep_visited) {
      $query->condition('operation', self::PAGE_VISITED_OPERATION, '!=');
    }
    $query->execute();
  }

@@ -318,13 +330,13 @@ class WebformNavigationHelper {
   *
   * @param \Drupal\webform\WebformInterface $webform
   *   A webform entity.
   * @param $element
   *   A webform element.
   * @param string $element
   *   The element's key.
   *
   * @return mixed
   *   A page an element belongs to.
   */
  public function getElementPage(WebformInterface $webform, $element) {
  public function getElementPage(WebformInterface $webform, string $element) {
    $element = $webform->getElement($element);
    return !empty($element) && array_key_exists('#webform_parents', $element) ? $element['#webform_parents'][0] : NULL;
  }
+3 −67
Original line number Diff line number Diff line
@@ -141,13 +141,13 @@ function webformnavigation_webform_third_party_settings_form_alter(&$form, FormS
    '#type' => 'webform_message',
    '#message_type' => 'warning',
    '#message_message' => t('You must enable the Webform Navigation submission handler under the <a href=":href">Emails / Handlers tab</a> for forward navigation to work.', [
      ':href' => $webform->toUrl('handlers')->toString()
      ':href' => $webform->toUrl('handlers')->toString(),
    ]),
    '#states' => [
      'visible' => [
        [':input[name="third_party_settings[webformnavigation][forward_navigation]"]' => ['checked' => TRUE]],
      ],
    ]
    ],
  ];
  $form['third_party_settings']['webformnavigation']['forward_navigation'] = [
    '#type' => 'checkbox',
@@ -173,71 +173,6 @@ function webformnavigation_webform_third_party_settings_form_alter(&$form, FormS
  ];
}

/**
 * Implements hook_webform_submission_form_alter().
 */
function webformnavigation_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
  $webform_submission = $form_state->getFormObject()->getEntity();
  $webform = $webform_submission->getWebform();
  // Get navigation webform settings.
  $prevent_next_validation = $webform->getThirdPartySetting('webformnavigation', 'prevent_next_validation');

  // Actions to perform if prevent_next_validation is set.
  if ($prevent_next_validation && isset($form['actions']['wizard_next'])) {
    $form['actions']['wizard_next']['#validate'][] = '::noValidate';
  }
}

/**
 * Programmatically validate a webform submission.
 *
 * @param array $form
 *   An associative array containing the structure of the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 *
 * @throws \Exception
 */
function webformnavigation_submission_validation(array &$form, FormStateInterface $form_state) {
  /** @var \Drupal\webformnavigation\WebformNavigationHelper $webformnavigation_helper */
  $webformnavigation_helper = Drupal::service('webformnavigation.helper');
  /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
  $webform_submission = $form_state->getFormObject()->getEntity();
  $webform = $webform_submission->getWebform();
  $has_errors = FALSE;
  // Ensure each page has been validated.
  $webformnavigation_helper->validateAllPages($webform_submission, $form_state);
  // Log the form errors.
  $webformnavigation_helper->logPageErrors($webform_submission, $form_state);
  // Clear errors and place the submission errors above them.
  $form_state->clearErrors();
  // Validate our submission values.
  $logged_errors = $webformnavigation_helper->getErrors($webform_submission);
  // Set form errors if the values are in-valid.
  foreach ($webform_submission->getWebform()->getPages('edit', $webform_submission) as $page_key => $page) {
    $page_errors = !empty($logged_errors[$page_key]) ? $logged_errors[$page_key] : [];
    if (!empty($page_errors)) {
      $has_errors = TRUE;
      // Set an error on the page to create separation in the error message.
      $page_message = t('<p class="webformnavigation-page-title"><strong>@title Page</strong></p>', ['@title' => $page['#title']]);
      $form_state->setErrorByName($page_key, $page_message);
      // Parse through the errors and set a form error for each.
      foreach ($page_errors as $field => $error) {
        $message = t('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;• @message', ['@message' => $error]);
        $form_state->setErrorByName($field, $message);
      }
    }
  }
  // Add additional error message if defined in the settings.
  if ($has_errors && !empty($webform->getThirdPartySetting('webformnavigation', 'additional_error_message'))) {
    $keys = array_keys($webform->getElementsInitializedAndFlattened());
    $additional_error_message = $webform->getThirdPartySetting('webformnavigation', 'additional_error_message');
    $message = t('<br><p class="webformnavigation-additional-message">@message</p>', ['@message' => $additional_error_message]);
    $form_state->setErrorByName($keys[1], $message);
  }
}

/**
 * Implements hook_entity_update().
 */
@@ -253,6 +188,7 @@ function webformnavigation_webform_presave(WebformInterface $webform) {
        case WebformSubmissionStorageInterface::PURGE_COMPLETED:
          $purge = WebformSubmissionStorageInterface::PURGE_ALL;
          break;

        default:
          $purge = WebformSubmissionStorageInterface::PURGE_DRAFT;
          break;