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

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

parent 1fc48633
Loading
Loading
Loading
Loading
+32 −20
Original line number Diff line number Diff line
@@ -2,8 +2,8 @@

namespace Drupal\webformnavigation\Plugin\WebformHandler;

use Drupal;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\webform\Plugin\WebformHandlerBase;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionInterface;
@@ -16,10 +16,12 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 *   id = "webform_navigation",
 *   label = @Translation("Webform Navigation"),
 *   category = @Translation("Webform"),
 *   description = @Translation("A webform submission handler for the webform navigation module."),
 *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_SINGLE,
 *   description = @Translation("A webform submission handler for the webform
 *   navigation module."), cardinality =
 *   \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_SINGLE,
 *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_IGNORED,
 *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_REQUIRED,
 *   submission =
 *   \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_REQUIRED,
 * )
 */
class WebformNavigationHandler extends WebformHandlerBase {
@@ -101,8 +103,6 @@ class WebformNavigationHandler extends WebformHandlerBase {

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  public function alterForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
    $this->debug(__FUNCTION__);
@@ -123,6 +123,11 @@ class WebformNavigationHandler extends WebformHandlerBase {
        if ($page_key != 'webform_confirmation') {
          $form['pages'][$page_key]['#access'] = TRUE;
          $form['pages'][$page_key]['#validate'] = $validations;
          $form['pages'][$page_key]['#submit'] = [
            '::submitForm',
            '::save',
          ];
          $form['pages'][$page_key]['#attributes']['formnovalidate'] = 'formnovalidate';
        }
      }
      // Set our loggers to the draft update if it is set.
@@ -144,12 +149,20 @@ class WebformNavigationHandler extends WebformHandlerBase {
        $this->webformNavigationHelper->logPageVisit($webform_submission, $current_page);
      }
      elseif ($current_page != 'webform_confirmation') {
        // Display any errors.
        $errors = $this->webformNavigationHelper->getErrors($webform_submission);
        // Make sure we haven't already set errors.
        if (!empty($errors[$current_page])) {
          foreach ($errors[$current_page] as $error) {
            Drupal::messenger()->addError($error);
        // Stash the current system errors.
        $current_system_error_messages = $this->webformNavigationHelper->getCurrentSystemErrors($webform_submission);
        // Just display the errors for current page.
        $this->messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
        $form_state->clearErrors();
        // Bring back the system errors.
        foreach ($current_system_error_messages as $error_message) {
          $this->messenger()->addError($error_message);
        }
        $logged_page_errors = $this->webformNavigationHelper->getErrors($webform_submission, $current_page);
        // Show the errors for the current page.
        if (!empty($logged_page_errors)) {
          foreach ($logged_page_errors as $logged_page_error) {
            $this->messenger()->addError($logged_page_error);
          }
        }
      }
@@ -158,8 +171,6 @@ class WebformNavigationHandler extends WebformHandlerBase {

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  public function validateForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
    $this->debug(__FUNCTION__);
@@ -167,18 +178,21 @@ class WebformNavigationHandler extends WebformHandlerBase {
    // Get navigation webform settings.
    $forward_navigation = $webform->getThirdPartySetting('webformnavigation', 'forward_navigation');
    // Actions to perform if forward navigation is enabled and there are pages.
    if ($forward_navigation && $webform->hasWizardPages()) {
    if ($forward_navigation) {
      $this->webformNavigationHelper->logPageErrors($webform_submission, $form_state);
    }
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
    $this->debug(__FUNCTION__);
    $triggering_element = $form_state->getTriggeringElement();
    $requested_page = !empty($triggering_element['#page']) ? $triggering_element['#page'] : NULL;
    if (!empty($requested_page) && $requested_page != 'webform_confirmation') {
      $webform_submission->setCurrentPage($requested_page);
    }
  }

  /**
@@ -234,8 +248,6 @@ class WebformNavigationHandler extends WebformHandlerBase {

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) {
    $this->debug(__FUNCTION__, $update ? 'update' : 'insert');
@@ -245,7 +257,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);
+65 −13
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';

@@ -149,7 +148,7 @@ class WebformNavigationHelper {
   * @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 [];
@@ -167,7 +166,10 @@ class WebformNavigationHelper {
    $query->range(0, 1);
    $submission_log = $query->execute()->fetch();
    $data = !empty($submission_log->data) ? unserialize($submission_log->data) : [];
    return (!empty($page) && !empty($data[$page])) ? $data[$page] : $data;
    if (!empty($page)) {
      return !empty($data[$page]) ? $data[$page] : [];
    }
    return $data;
  }

  /**
@@ -196,9 +198,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 +217,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)) {
@@ -260,7 +262,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 +290,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();
    }
@@ -376,7 +378,7 @@ class WebformNavigationHelper {
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  private function validateSinglePage(WebformSubmissionInterface $webform_submission, string $page) {
  public function validateSinglePage(WebformSubmissionInterface $webform_submission, string $page) {
    // Stash the current page.
    $current_page = $webform_submission->getCurrentPage();
    // Let's ensure we are on the page that needs to be validated.
@@ -388,14 +390,64 @@ class WebformNavigationHelper {
    // Create an empty form state which will be populated when the submission
    // form is submitted.
    $new_form_state = new FormState();
    // Lets make sure we don't create a validation loop.
    // Let's make sure we don't create a validation loop.
    $new_form_state->set('validating', TRUE);
    // Submit the form.
    $this->form_builder->submitForm($form_object, $new_form_state);
    if (!$this->hasVisitedPage($webform_submission, $current_page)) {
      $this->logPageVisit($webform_submission, $page);
    }
    $this->logPageErrors($webform_submission, $new_form_state);
    // Return to the original page.
    $webform_submission->setCurrentPage($current_page);
  }

  /**
   * Gets all the none webform related error messages.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   The webform submission.
   *
   * @return array
   *   An array of error messages.
   */
  public function getCurrentSystemErrors(WebformSubmissionInterface $webform_submission) {
    $logged_errors = $this->getErrors($webform_submission) ?? [];
    $current_error_messages = $this->messenger->deleteByType(MessengerInterface::TYPE_ERROR) ?? [];
    $page_names = $this->getFormPageNames($webform_submission->getWebform());
    if (!empty($logged_errors)) {
      foreach ($page_names as $page_name) {
        if (!empty($logged_errors[$page_name])) {
          foreach ($logged_errors[$page_name] as $logged_error) {
            $rendered_logged_error = $logged_error->render();
            if (in_array($rendered_logged_error, $current_error_messages)) {
              $pos = array_search($rendered_logged_error, $current_error_messages);
              unset($current_error_messages[$pos]);
            }
          }
        }
      }
    }
    return $current_error_messages;
  }

  /**
   * Gets all the non-confirmation page names.
   *
   * @param \Drupal\webform\WebformInterface $webform
   *   The webform.
   *
   * @return string[]
   *   The array of page names.
   */
  public function getFormPageNames(WebformInterface $webform) {
    $all_pages = $webform->getPages();
    foreach ($all_pages as $name => $page) {
      if ($name !== 'webform_confirmation') {
        $form_pages[] = $name;
      }
    }
    return $form_pages ?? [];
  }

}
+35 −27
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@
 */

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionStorageInterface;
@@ -72,7 +73,7 @@ function webformnavigation_preprocess_webform_progress_tracker(&$variables) {
    $pages = $webform->getPages($variables['operation'], $webform_submission);
    // Get the logged errors for the form.
    /** @var \Drupal\webformnavigation\WebformNavigationHelper $webformnavigation_helper */
    $webformnavigation_helper = Drupal::service('webformnavigation.helper');
    $webformnavigation_helper = \Drupal::service('webformnavigation.helper');
    $current_errors = $webformnavigation_helper->getErrors($webform_submission);
    // Iterate through the pages and set appropriate page classes.
    foreach ($pages as $key => $page) {
@@ -106,7 +107,7 @@ function webformnavigation_preprocess_webform_progress_bar(&$variables) {
    $pages = $webform->getPages($variables['operation'], $webform_submission);
    // Get the logged errors for the form.
    /** @var \Drupal\webformnavigation\WebformNavigationHelper $webformnavigation_helper */
    $webformnavigation_helper = Drupal::service('webformnavigation.helper');
    $webformnavigation_helper = \Drupal::service('webformnavigation.helper');
    $current_errors = $webformnavigation_helper->getErrors($webform_submission);
    // Iterate through the pages and set appropriate page classes.
    foreach ($pages as $key => $page) {
@@ -141,13 +142,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',
@@ -201,41 +202,47 @@ function webformnavigation_webform_submission_form_alter(array &$form, FormState
 */
function webformnavigation_submission_validation(array &$form, FormStateInterface $form_state) {
  /** @var \Drupal\webformnavigation\WebformNavigationHelper $webformnavigation_helper */
  $webformnavigation_helper = Drupal::service('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.
  // Get the page names we need.
  $pages = $webformnavigation_helper->getFormPageNames($webform);
  // Stash the system errors.
  $system_error_messages = $webformnavigation_helper->getCurrentSystemErrors($webform_submission);
  // Clear our errors.
  $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)) {
  \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
  // Validate all the pages.
  $webformnavigation_helper->validateAllPages($webform_submission, $form_state);
  // Add each page's error messages.
  foreach ($pages as $page) {
    // Get the errors for the page.
    $logged_errors = $webformnavigation_helper->getErrors($webform_submission, $page);
    // Add the errors to a list of errors per page.
    if (!empty($logged_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);
      $page_errors = [
        '#theme' => 'item_list',
        '#items' => $logged_errors,
        '#title' => $webform->getPage('edit', $page)['#title'],
      ];
      $form_state->setErrorByName($page, $page_errors);
    }
  }
  // Bring back the system errors.
  foreach ($system_error_messages as $error_message) {
    \Drupal::messenger()->addError($error_message);
  }
  // Add additional error message if defined in the settings.

  // Add the 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);
    $form_state->setError($form, $message);
  }

}

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

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