<?php

namespace Drupal\webform;

use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\webform\Cache\WebformBubbleableMetadata;
use Drupal\webform\Entity\WebformSubmission;
use Drupal\webform\Form\WebformDialogFormTrait;
use Drupal\webform\Plugin\WebformElement\Hidden;
use Drupal\webform\Plugin\WebformElement\OptionsBase;
use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
use Drupal\webform\Plugin\WebformElementOtherInterface;
use Drupal\webform\Plugin\WebformElementWizardPageInterface;
use Drupal\webform\Plugin\WebformHandlerInterface;
use Drupal\webform\Plugin\WebformSourceEntityManager;
use Drupal\webform\Utility\WebformArrayHelper;
use Drupal\webform\Utility\WebformDialogHelper;
use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\Utility\WebformOptionsHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Provides a webform to collect and edit submissions.
 */
class WebformSubmissionForm extends ContentEntityForm {

  use WebformDialogFormTrait;

  /**
   * The configuration object factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * The kill switch.
   *
   * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch
   */
  protected $killSwitch;

  /**
   * The path alias manager.
   *
   * @var \Drupal\path_alias\AliasManagerInterface
   */
  protected $aliasManager;

  /**
   * The path validator service.
   *
   * @var \Drupal\Core\Path\PathValidatorInterface
   */
  protected $pathValidator;

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * The selection plugin manager service.
   *
   * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
   */
  protected $selectionManager;

  /**
   * The webform element plugin manager.
   *
   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
   */
  protected $elementManager;

  /**
   * The webform request handler.
   *
   * @var \Drupal\webform\WebformRequestInterface
   */
  protected $requestHandler;

  /**
   * The webform third party settings manager.
   *
   * @var \Drupal\webform\WebformThirdPartySettingsManagerInterface
   */
  protected $thirdPartySettingsManager;

  /**
   * The webform message manager.
   *
   * @var \Drupal\webform\WebformMessageManagerInterface
   */
  protected $messageManager;

  /**
   * The webform token manager.
   *
   * @var \Drupal\webform\WebformTokenManagerInterface
   */
  protected $tokenManager;

  /**
   * The webform submission conditions (#states) validator.
   *
   * @var \Drupal\webform\WebformSubmissionConditionsValidatorInterface
   */
  protected $conditionsValidator;

  /**
   * The webform entity reference manager.
   *
   * @var \Drupal\webform\WebformEntityReferenceManagerInterface
   */
  protected $webformEntityReferenceManager;

  /**
   * The webform submission generation service.
   *
   * @var \Drupal\webform\WebformSubmissionGenerateInterface
   */
  protected $generate;

  /**
   * The webform submission.
   *
   * @var \Drupal\webform\WebformSubmissionInterface
   */
  protected $entity;

  /**
   * The source entity.
   *
   * @var \Drupal\Core\Entity\EntityInterface
   */
  protected $sourceEntity;

  /**
   * States API prefix.
   *
   * @var string
   */
  protected $statesPrefix;

  /**
   * Stores the original submission data passed via the EntityFormBuilder.
   *
   * @var array
   *
   * @see \Drupal\webform\WebformSubmissionForm::setEntity
   */
  protected $originalData;

  /**
   * Bubbleable metadata.
   *
   * @var \Drupal\webform\Cache\WebformBubbleableMetadata
   *
   * @see \Drupal\webform\WebformSubmissionForm::buildForm
   */
  protected $bubbleableMetadata;

  /**
   * Operation value, like 'default', add, edit, test, etc.
   *
   * @var string
   */
  protected $operation;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $instance = parent::create($container);
    $instance->configFactory = $container->get('config.factory');
    $instance->renderer = $container->get('renderer');
    $instance->formBuilder = $container->get('form_builder');
    $instance->killSwitch = $container->get('page_cache_kill_switch');
    $instance->aliasManager = $container->get('path_alias.manager');
    $instance->pathValidator = $container->get('path.validator');
    $instance->selectionManager = $container->get('plugin.manager.entity_reference_selection');
    $instance->entityFieldManager = $container->get('entity_field.manager');
    $instance->requestHandler = $container->get('webform.request');
    $instance->elementManager = $container->get('plugin.manager.webform.element');
    $instance->thirdPartySettingsManager = $container->get('webform.third_party_settings_manager');
    $instance->messageManager = $container->get('webform.message_manager');
    $instance->tokenManager = $container->get('webform.token_manager');
    $instance->conditionsValidator = $container->get('webform_submission.conditions_validator');
    $instance->webformEntityReferenceManager = $container->get('webform.entity_reference_manager');
    $instance->generate = $container->get('webform_submission.generate');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function getBaseFormId() {
    $base_form_id = $this->entity->getEntityTypeId();
    $base_form_id .= '_' . $this->entity->bundle();
    return $base_form_id . '_form';
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    $form_id = $this->entity->getEntityTypeId();
    $form_id .= '_' . $this->entity->bundle();
    if ($source_entity = $this->entity->getSourceEntity()) {
      $form_id .= '_' . $source_entity->getEntityTypeId() . '_' . $source_entity->id();
    }
    if ($this->operation !== 'default') {
      $form_id .= '_' . $this->operation;
    }
    return $form_id . '_form';
  }

  /**
   * {@inheritdoc}
   */
  protected function getWrapperId() {
    // Get the form id with the source entity but without the operation.
    $form_id = $this->entity->getEntityTypeId();
    $form_id .= '_' . $this->entity->bundle();
    if ($source_entity = $this->entity->getSourceEntity()) {
      $form_id .= '_' . $source_entity->getEntityTypeId() . '_' . $source_entity->id();
    }
    $form_id .= '_form';
    return Html::getId($form_id . '-ajax');
  }

  /**
   * {@inheritdoc}
   */
  protected function init(FormStateInterface $form_state) {
    parent::init($form_state);
    $this->getWebform()->invokeHandlers('prepareForm', $this->entity, $this->operation, $form_state);
  }

  /**
   * {@inheritdoc}
   *
   * This is the best place to override an entity form's default settings
   * because it is called immediately after the form object is initialized.
   *
   * @see \Drupal\Core\Entity\EntityFormBuilder::getForm
   */
  public function setEntity(EntityInterface $entity) {
    // Create new metadata to be applie when the form is built.
    // @see \Drupal\webform\WebformSubmissionForm::buildForm
    $this->bubbleableMetadata = new WebformBubbleableMetadata();

    /** @var \Drupal\webform\WebformSubmissionInterface $entity */
    $webform = $entity->getWebform();

    // Initialize the webform submission entity by getting it default data
    // and storing its original data.
    if (!isset($this->originalData)) {
      // Store the original data passed via the EntityFormBuilder.
      // This allows us to reset the submission to it's original state
      // via ::reset.
      // @see \Drupal\Core\Entity\EntityFormBuilder::getForm
      // @see \Drupal\webform\Entity\Webform::getSubmissionForm
      // @see \Drupal\webform\WebformSubmissionForm::reset
      $this->originalData = $entity->getRawData();
    }

    // Get the submission data and only call WebformSubmission::setData once.
    $data = $entity->getRawData();

    // If ?_webform_test is defined for the current webform, override
    // the 'add' operation with 'test' operation.
    if ($this->operation === 'add' &&
      $this->getRequest()->query->get('_webform_test') === $webform->id() &&
      $webform->access('test')
    ) {
      $this->operation = 'test';
    }

    // Generate test data.
    if ($this->operation === 'test'
      && $webform->access('test')) {
      $webform->applyVariants($entity);
      $data = $webform->getVariantsData($entity)
        + $this->generate->getData($webform)
        + $data;
    }

    // Get the source entity and allow webform submission to be used as a source
    // entity.
    $source_entity = $entity->getSourceEntity(TRUE) ?: $this->requestHandler->getCurrentSourceEntity(['webform']);
    if ($source_entity === $entity) {
      $source_entity = $this->requestHandler->getCurrentSourceEntity(['webform', 'webform_submission']);
    }
    // Handle paragraph source entity.
    if ($source_entity && $source_entity->getEntityTypeId() === 'paragraph') {
      // Disable :clear suffix to prevent webform tokens from being removed.
      $data = $this->tokenManager->replace($data, $source_entity, [], ['suffixes' => ['clear' => FALSE]], $this->bubbleableMetadata);
      $source_entity = WebformSourceEntityManager::getMainSourceEntity($source_entity);
    }
    // Set source entity.
    $this->sourceEntity = $source_entity;

    // Get account.
    $account = $this->currentUser();

    // Load entity from token or saved draft when not editing or testing
    // submission form.
    if (!in_array($this->operation, ['edit', 'edit_all', 'test'])) {
      $token = $this->getRequest()->query->get('token');
      $webform_submission_token = $this->getStorage()->loadFromToken($token, $webform, $source_entity);
      if ($webform_submission_token) {
        $entity = $webform_submission_token;
        $data = $entity->getRawData();
        $this->operation = 'edit';
      }
      elseif ($webform->getSetting('draft') !== WebformInterface::DRAFT_NONE) {
        if ($webform->getSetting('draft_multiple')) {
          // Allow multiple drafts to be restored using token.
          // This allows the webform's public facing URL to be used instead of
          // the admin URL of the webform.
          $webform_submission_token = $this->getStorage()->loadFromToken($token, $webform, $source_entity, $account);
          $request_token = $this->requestStack->getCurrentRequest()->request->get('webform_submission_token');
          if ($webform_submission_token && $webform_submission_token->isDraft()) {
            $entity = $webform_submission_token;
            $data = $entity->getRawData();
          }
          elseif ($request_token) {
            $webform_submission_token = $this->getStorage()->loadFromToken($request_token, $webform, $source_entity, $account);
            if ($webform_submission_token && $webform_submission_token->isDraft()) {
              $entity = $webform_submission_token;
              $data = $entity->getRawData();
            }
          }
        }
        elseif ($webform_submission_draft = $this->getStorage()->loadDraft($webform, $source_entity, $account)) {
          // Else load the most recent draft.
          $entity = $webform_submission_draft;
          $data = $entity->getRawData();
        }
      }
    }

    // Set entity before calling get last submission.
    $this->entity = $entity;

    if ($entity->isNew()) {
      $last_submission = NULL;
      if ($webform->getSetting('limit_total_unique')) {
        // Require user to have update any submission access.
        if (!$webform->access('submission_view_any')
          || !$webform->access('submission_update_any')) {
          throw new AccessDeniedHttpException();
        }
        // Get last webform/source entity submission.
        $last_submission = $this->getStorage()->getLastSubmission($webform, $source_entity, NULL, ['in_draft' => FALSE]);
      }
      elseif ($webform->getSetting('limit_user_unique')) {
        // Require user to be authenticated to access a unique submission.
        if (!$account->isAuthenticated()) {
          throw new AccessDeniedHttpException();
        }
        // Require user to have update own submission access.
        if (!$webform->access('submission_view_own')
          || !$webform->access('submission_update_own')) {
          throw new AccessDeniedHttpException();
        }
        // Get last user submission.
        $last_submission = $this->getStorage()->getLastSubmission($webform, $source_entity, $account, ['in_draft' => FALSE]);
      }

      // If the webform is closed and user can not update any submission,
      // block the submission from being updated.
      if ($webform->isClosed() && !$webform->access('submission_update_any')) {
        $last_submission = NULL;
      }

      // Set last submission and switch to the edit operation.
      if ($last_submission) {
        $entity = $last_submission;
        $data = $entity->getRawData();
        $this->operation = 'edit';
      }
    }

    // Autofill with previous submission.
    if ($this->operation === 'add'
      && $entity->isNew()
      && $webform->getSetting('autofill')) {
      $data = $this->getLastSubmissionData($webform, $source_entity, $account) + $data;
    }

    // Get default data and append it to the submission's data.
    // This allows computed elements to be executed and tokens
    // to be replaced using the webform's default data.
    $default_data = $webform->getElementsDefaultData();
    $default_data = $this->tokenManager->replace($default_data, $entity, [], [], $this->bubbleableMetadata);
    $data += $default_data;

    // Set data and calculate computed values.
    $entity->setData($data);

    // Override settings.
    $this->overrideSettings($entity);

    // Set the webform's current operation.
    $webform->setOperation($this->operation);

    return parent::setEntity($entity);
  }

  /**
   * Get last submission data with excluded elements.
   *
   * @param \Drupal\webform\WebformInterface $webform
   *   A webform.
   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
   *   (optional) A webform submission source entity.
   * @param \Drupal\Core\Session\AccountInterface|null $account
   *   The current user account.
   *
   * @return array
   *   An associative array containing last submission data
   *   with excluded elements.
   */
  protected function getLastSubmissionData(WebformInterface $webform, EntityInterface $source_entity = NULL, AccountInterface $account = NULL) {
    $last_submission = $this->getStorage()->getLastSubmission($webform, $source_entity, $account, ['in_draft' => FALSE, 'access_check' => FALSE]);
    if (!$last_submission) {
      return [];
    }

    $data = $last_submission->getRawData();
    $excluded_elements = $webform->getSetting('autofill_excluded_elements') ?: [];
    foreach ($excluded_elements as $excluded_element_key) {
      // Unset excluded element.
      unset($data[$excluded_element_key]);

      // Unset excluded composite sub-element.
      if (strpos($excluded_element_key, '__') !== FALSE) {
        [$excluded_parent_key, $excluded_composite_key] = explode('__', $excluded_element_key);
        if (isset($data[$excluded_parent_key]) && is_array($data[$excluded_parent_key])) {
          if (WebformArrayHelper::isSequential($data[$excluded_parent_key])) {
            // Multi-value composite.
            foreach (array_keys($data[$excluded_parent_key]) as $delta) {
              unset($data[$excluded_parent_key][$delta][$excluded_composite_key]);
            }
          }
          else {
            // Single-value composite.
            unset($data[$excluded_parent_key][$excluded_composite_key]);
          }
        }
      }
    }
    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function buildEntity(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $entity */
    $entity = parent::buildEntity($form, $form_state);

    // Override settings.
    $this->overrideSettings($entity);

    return $entity;
  }

  /**
   * Override webform settings for the webform submission.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   */
  protected function overrideSettings(WebformSubmissionInterface $webform_submission) {
    $webform = $webform_submission->getWebform();

    // Invoke override settings which resets the webform settings.
    $webform->invokeHandlers('overrideSettings', $webform_submission);

    // Look for ?_webform_dialog=1 which enables Ajax support when this form is
    // opened in dialog.
    // @see webform.dialog.js
    //
    // Must be called after WebformHandler::overrideSettings which resets all
    // overridden settings.
    // @see \Drupal\webform\Entity\Webform::invokeHandlers
    if ($this->getRequest()->query->get('_webform_dialog') && !$webform->getSetting('ajax', TRUE)) {
      $webform->setSettingOverride('ajax', TRUE);
    }

    // If share page (i.e. /webform_share/{webform}), enable Ajax support
    // when this form is embedded in an iframe.
    if ($this->isSharePage() && !$webform->getSetting('ajax', TRUE)) {
      $webform->setSettingOverride('ajax', TRUE);
    }

    // Apply source entity open/close state to the webform before
    // it is rendered.
    $source_entity = $webform_submission->getSourceEntity();
    if ($webform->isOpen() && $source_entity && $source_entity instanceof FieldableEntityInterface) {
      foreach ($source_entity->getFieldDefinitions() as $fieldName => $fieldDefinition) {
        if ($fieldDefinition->getType() === 'webform') {
          $item = $source_entity->get($fieldName);
          if ($item->target_id === $webform->id()) {
            $webform
              ->setOverride()
              ->set('open', $item->open)
              ->set('close', $item->close)
              ->setStatus($item->status);
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
    // NOTE: We are not copying form values to the entity because
    // webform element keys can override webform submission properties.
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $entity;
    $webform = $webform_submission->getWebform();

    // Get elements values from webform submission and merge existing data.
    $element_values = array_intersect_key(
      $form_state->getValues(),
      $webform->getElementsInitializedFlattenedAndHasValue()
    );
    $webform_submission->setData($element_values + $webform_submission->getData());

    // Get field values.
    // This used to support the Workflows Field module.
    // @see https://www.drupal.org/project/webform/issues/3002547
    // @see https://www.drupal.org/project/workflows_field
    $base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity->getEntityTypeId());
    $all_fields = $webform_submission->getFields(FALSE);
    $bundle_fields = array_diff_key($all_fields, $base_field_definitions);
    $field_values = array_intersect_key($form_state->getValues(), $bundle_fields);
    foreach ($field_values as $name => $field_value) {
      $webform_submission->set($name, $field_value);
    }

    // Set current page.
    if ($current_page = $this->getCurrentPage($form, $form_state)) {
      $entity->setCurrentPage($current_page);
    }

    // Set in draft.
    $in_draft = $form_state->get('in_draft');
    if ($in_draft !== NULL) {
      $entity->set('in_draft', $in_draft);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    $webform = $this->getWebform();

    // Track the 'webform_submission_token' form multiple draft submissions.
    // The 'webform_submission_token' value is set after the form is cached
    // and built.
    // @see \Drupal\webform\WebformSubmissionForm::afterBuild
    if ($webform->getSetting('draft_multiple')
      && ($webform_submission->isNew() || $webform_submission->isDraft())) {
      $form['webform_submission_token'] = [
        '#type' => 'hidden',
        '#value' => $webform_submission->getToken(),
      ];
    }

    // Only prepopulate data when a webform is initially loaded or ajax is
    // triggering a restart.
    if (!$form_state->isRebuilding() || $form_state->get('is_ajax_restart')) {
      $form_state->set('is_ajax_restart', FALSE);

      $data = $webform_submission->getData();
      $this->prepopulateData($data);
      $webform_submission->setData($data);
    }

    // Apply variants.
    $webform->applyVariants($webform_submission);

    // Add cache dependency to the form's render array.
    $this->addCacheableDependency($form);

    // Add the webform as a cacheable dependency.
    $this->renderer->addCacheableDependency($form, $webform);

    // Kill page cache for scheduled webforms.
    // @todo Remove once bubbling of element's max-age to page cache is fixed.
    // @see https://www.drupal.org/project/webform/issues/3015760
    // @see https://www.drupal.org/project/drupal/issues/2352009
    // @see \Drupal\webform\Element\Webform::preRenderWebformElement
    if ($webform->isScheduled()
      && $this->currentUser()->isAnonymous()
      && $this->moduleHandler->moduleExists('page_cache')) {
      $this->killSwitch->trigger();
    }

    // Display status messages.
    $this->displayMessages($form, $form_state);

    // Build the webform.
    $form = parent::buildForm($form, $form_state);

    // Ajax: Scroll to.
    // @see \Drupal\webform\Form\WebformAjaxFormTrait::submitAjaxForm
    if ($this->isAjax()) {
      $form['#webform_ajax_scroll_top'] = $this->getWebformSetting('ajax_scroll_top', '');
    }

    // Alter element's form.
    if (isset($form['elements']) && is_array($form['elements'])) {
      $elements = $form['elements'];
      $this->alterElementsForm($elements, $form, $form_state);
    }

    // Add Ajax callbacks.
    $ajax_settings = [
      'effect' => $this->getWebformSetting('ajax_effect'),
      'speed' => (int) $this->getWebformSetting('ajax_speed'),
      'progress' => [
        'type' => $this->getWebformSetting('ajax_progress_type'),
        'message' => '',
      ],
    ];
    $form = $this->buildAjaxForm($form, $form_state, $ajax_settings);

    // Alter webform via webform handler.
    $this->getWebform()->invokeHandlers('alterForm', $form, $form_state, $webform_submission);

    // Call custom webform alter hook.
    $form_id = $this->getFormId();
    $this->thirdPartySettingsManager->alter('webform_submission_form', $form, $form_state, $form_id);

    // Server side #states API validation.
    $this->conditionsValidator->buildForm($form, $form_state);

    // Append the bubbleable metadat to the form's render array.
    // @see \Drupal\webform\WebformSubmissionForm::setEntity
    $this->bubbleableMetadata->appendTo($form);

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    $source_entity = $webform_submission->getSourceEntity();
    $webform = $this->getWebform();

    // Add a reference to the webform's id to the $form render array.
    $form['#webform_id'] = $webform->id();

    // Move form method and action to form properties.
    $form_method = $this->getWebformSetting('form_method');
    $form_action = $this->getWebformSetting('form_action');
    if ($form_method && $form_action) {
      $form['#method'] = $form_method;
      $form['#action'] = $form_action;
    }

    // Move form attributes to form properties.
    $form_attributes = $this->getWebformSetting('form_attributes');
    if ($form_attributes) {
      $form['#attributes'] = $form_attributes;
    }

    // Track current page name or index by setting the
    // "data-webform-wizard-page"
    // attribute which is used Drupal.behaviors.webformWizardTrackPage.
    //
    // The data parameter is append to the URL after the form has
    // been submitted.
    //
    // @see js/webform.wizard.track.js
    $track = $this->getWebform()->getSetting('wizard_track');
    $current_page = $this->getCurrentPage($form, $form_state);
    if ($track && $current_page !== '' && $this->getRequest()->isMethod('POST')) {
      if ($track === 'index') {
        $pages = $this->getWebform()->getPages($this->operation);
        $track_pages = array_flip(array_keys($pages));
        $form['#attributes']['data-webform-wizard-current-page'] = ($track_pages[$current_page] + 1);
      }
      else {
        $form['#attributes']['data-webform-wizard-current-page'] = $current_page;
      }
    }

    // Disable the default $form['#theme'] templates.
    // If the webform's id begins with an underscore the #theme
    // was automatically being set to 'webform_submission__WEBFORM_ID', this
    // causes the form to be rendered using the 'webform_submission' template.
    // @see \Drupal\Core\Form\FormBuilder::prepareForm
    // @see webform-submission-form.html.twig
    $form['#theme'] = ['webform_submission_form'];

    // Define very specific webform classes, this override the form's
    // default classes.
    // @see \Drupal\Core\Form\FormBuilder::retrieveForm
    $webform_id = Html::cleanCssIdentifier($webform->id());
    $operation = $this->operation;
    $class = [];
    $class[] = "webform-submission-form";
    $class[] = "webform-submission-$operation-form";
    $class[] = "webform-submission-$webform_id-form";
    $class[] = "webform-submission-$webform_id-$operation-form";
    if ($source_entity) {
      $source_entity_type = $source_entity->getEntityTypeId();
      $source_entity_id = $source_entity->id();
      $class[] = "webform-submission-$webform_id-$source_entity_type-$source_entity_id-form";
      $class[] = "webform-submission-$webform_id-$source_entity_type-$source_entity_id-$operation-form";
    }
    array_walk($class, ['\Drupal\Component\Utility\Html', 'getClass']);
    $form += ['#attributes' => []];
    $form['#attributes'] += ['class' => []];
    $form['#attributes']['class'] = array_merge($class, $form['#attributes']['class']);

    // Get last class, which is the most specific, as #states prefix.
    // @see \Drupal\webform\WebformSubmissionForm::addStatesPrefix
    $this->statesPrefix = '.' . end($class);

    // Check for a custom webform, track it, and return it.
    if ($custom_form = $this->getCustomForm($form, $form_state)) {
      $custom_form['#custom_form'] = TRUE;
      return $custom_form;
    }

    $form = parent::form($form, $form_state);

    /* Information */

    // Prepend webform submission data using the default view without the data.
    if (!$webform_submission->isNew() && !$webform_submission->isDraft()) {
      $form['navigation'] = [
        '#type' => 'webform_submission_navigation',
        '#webform_submission' => $webform_submission,
        '#weight' => -20,
      ];
      $form['information'] = [
        '#type' => 'webform_submission_information',
        '#webform_submission' => $webform_submission,
        '#source_entity' => $this->sourceEntity,
        '#weight' => -19,
      ];
    }

    /* Confirmation */

    // Add confirmation modal.
    if ($webform_confirmation_modal = $form_state->get('webform_confirmation_modal')) {
      $form['webform_confirmation_modal'] = [
        '#type' => 'webform_message',
        '#message_type' => 'status',
        '#message_message' => [
          'title' => [
            '#markup' => $webform_confirmation_modal['title'],
            '#prefix' => '<b class="webform-confirmation-modal--title">',
            '#suffix' => '</b><br/>',
          ],
          'content' => [
            'content' => $webform_confirmation_modal['content'],
            '#prefix' => '<div class="webform-confirmation-modal--content">',
            '#suffix' => '</div>',
          ],
        ],
        '#attributes' => ['class' => ['js-hide', 'webform-confirmation-modal', 'js-webform-confirmation-modal']],
        '#weight' => -1000,
        '#attached' => ['library' => ['webform/webform.confirmation.modal']],
        '#element_validate' => ['::removeConfirmationModal'],
      ];
    }

    /* Data */

    // Get and prepopulate (via query string) submission data.
    $data = $webform_submission->getData();

    /* Elements */

    // Get webform elements.
    $elements = $webform_submission->getWebform()->getElementsInitialized();

    // Populate webform elements with webform submission data.
    $this->populateElements($elements, $data);

    // Prepare webform elements.
    $this->prepareElements($elements, $form, $form_state);

    // Add wizard progress tracker and page links to the webform.
    $pages = $webform->getPages($this->operation);
    if ($pages && $operation !== 'edit_all') {
      $current_page = $this->getCurrentPage($form, $form_state);

      // Add hidden pages submit actions.
      $form['pages'] = $this->pagesElement($form, $form_state);

      // Add progress tracker.
      $display_wizard_progress = ($this->getWebformSetting('wizard_progress_bar') || $this->getWebformSetting('wizard_progress_pages') || $this->getWebformSetting('wizard_progress_percentage'));
      if ($current_page && $display_wizard_progress) {
        $form['progress'] = [
          '#theme' => 'webform_progress',
          '#webform' => $this->getWebform(),
          '#webform_submission' => $webform_submission,
          '#current_page' => $current_page,
          '#operation' => $this->operation,
          '#weight' => -20,
        ];
      }
    }

    // Required indicator.
    $current_page = $this->getCurrentPage($form, $form_state);
    if ($current_page !== WebformInterface::PAGE_PREVIEW && $this->getWebformSetting('form_required') && $webform->hasRequired()) {
      $form['required'] = [
        '#theme' => 'webform_required',
        '#label' => $this->getWebformSetting('form_required_label'),
      ];
    }

    // Append elements to the webform.
    $form['elements'] = $elements;

    // Pages: Set current wizard or preview page.
    $this->displayCurrentPage($form, $form_state);

    // Move all $elements properties to the $form.
    $this->setFormPropertiesFromElements($form, $elements);

    // Attach libraries to the form.
    $this->attachLibraries($form, $form_state);

    // Attach behaviors to the form.
    $this->attachBehaviors($form, $form_state);

    // Add #after_build callbacks.
    $form['#after_build'][] = '::afterBuild';

    return $form;
  }

  /**
   * Get custom webform which is displayed instead of the webform's elements.
   *
   * @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.
   *
   * @return array|bool
   *   A custom webform or FALSE if the default webform containing the webform's
   *   elements should be built.
   */
  protected function getCustomForm(array &$form, FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    $webform = $this->getWebform();

    // Exit if elements are broken, usually occurs when elements YAML is edited
    // directly in the export config file.
    if (!$webform->getElementsInitialized()) {
      // Display helpful message to user who can add elements to the webform.
      if (empty($webform->getElementsDecoded()) && $webform->access('update')) {
        $form['webform_message'][] = [
          '#type' => 'webform_message',
          '#message_type' => 'warning',
          '#message_message' => [
            'message' => [
              '#markup' => $this->t('This webform has no elements added to it.'),
              '#suffix' => '<br/>',
            ],
            'link' => [
              '#type' => 'link',
              '#title' => $this->t('Please add elements to this webform.'),
              '#url' => $webform->toUrl('edit-form'),
            ],
          ],
        ];
        return $form;
      }
      else {
        return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_EXCEPTION_MESSAGE, 'warning');
      }
    }

    // Exit if submission is locked.
    if ($webform_submission->isLocked()) {
      return $this->getMessageManager()->append($form, WebformMessageManagerInterface::SUBMISSION_LOCKED_MESSAGE, 'warning');
    }

    // Check prepopulate source entity required and type.
    if ($webform->getSetting('form_prepopulate_source_entity')) {
      if ($webform->getSetting('form_prepopulate_source_entity_required') && empty($this->getSourceEntity())) {
        $this->getMessageManager()->log(WebformMessageManagerInterface::PREPOPULATE_SOURCE_ENTITY_REQUIRED, 'notice');
        return $this->getMessageManager()->append($form, WebformMessageManagerInterface::PREPOPULATE_SOURCE_ENTITY_REQUIRED, 'warning');
      }
      $source_entity_type = $webform->getSetting('form_prepopulate_source_entity_type');
      if ($source_entity_type && $this->getSourceEntity() && $source_entity_type !== $this->getSourceEntity()->getEntityTypeId()) {
        $this->getMessageManager()->log(WebformMessageManagerInterface::PREPOPULATE_SOURCE_ENTITY_TYPE, 'notice');
        return $this->getMessageManager()->append($form, WebformMessageManagerInterface::PREPOPULATE_SOURCE_ENTITY_TYPE, 'warning');
      }
    }

    // Display inline confirmation message with back to link.
    if ($form_state->get('current_page') === WebformInterface::PAGE_CONFIRMATION) {
      $form['confirmation'] = [
        '#theme' => 'webform_confirmation',
        '#webform' => $webform,
        '#source_entity' => $webform_submission->getSourceEntity(TRUE),
        '#webform_submission' => $webform_submission,
      ];
      // Add hidden back (aka reset) button used by the Ajaxified back to link.
      // NOTE: Below code could be used to add a 'Reset' button to any webform.
      // @see Drupal.behaviors.webformConfirmationBackAjax
      $form['actions'] = [
        '#type' => 'actions',
        '#attributes' => ['style' => 'display:none'],
        // Do not process the actions element. This prevents the
        // .form-actions class from being added, which then makes the button
        // appear in dialogs.
        // @see \Drupal\Core\Render\Element\Actions::processActions
        // @see Drupal.behaviors.dialog.prepareDialogButtons
        '#process' => [],
      ];
      $form['actions']['reset'] = [
        '#type' => 'submit',
        '#value' => $this->t('Reset'),
        // @see \Drupal\webform\WebformSubmissionForm::noValidate
        '#validate' => ['::noValidate'],
        '#submit' => ['::reset'],
        '#attributes' => [
          'style' => 'display:none',
          'class' => ['js-webform-confirmation-back-submit-ajax'],
        ],
      ];
      return $form;
    }

    // Don't display webform if it is closed.
    if (($webform_submission->isNew() || $webform_submission->isDraft()) && $webform->isClosed()) {
      // If the current user can update any submission just display the closed
      // message and still allow them to create new submissions.
      if ($webform->isTemplate() && $webform->access('duplicate') && !$webform->isArchived()) {
        if (!$this->isDialog()) {
          $this->getMessageManager()->display(WebformMessageManagerInterface::TEMPLATE_PREVIEW, 'warning');
        }
      }
      elseif ($webform->access('submission_update_any')) {
        $form = $this->getMessageManager()->append($form, $webform->isArchived() ? WebformMessageManagerInterface::ADMIN_ARCHIVED : WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
      }
      else {
        if ($webform->isOpening()) {
          return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_OPEN_MESSAGE);
        }
        else {
          return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_CLOSE_MESSAGE);
        }
      }
    }

    // Disable this webform if confidential and user is logged in.
    if ($this->isConfidential()
      && $this->currentUser()->isAuthenticated()
      && $this->entity->isNew()
      && $this->operation === 'add') {
      return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_CONFIDENTIAL_MESSAGE, 'warning');
    }

    // Disable this webform if submissions are not being saved to the database or
    // passed to a WebformHandler.
    if ($this->getWebformSetting('results_disabled') && !$this->getWebformSetting('results_disabled_ignore') && !$webform->getHandlers(NULL, TRUE, WebformHandlerInterface::RESULTS_PROCESSED)->count()) {
      $this->getMessageManager()->log(WebformMessageManagerInterface::FORM_SAVE_EXCEPTION, 'error');
      if ($this->currentUser()->hasPermission('administer webform')) {
        // Display error to admin but allow them to submit the broken webform.
        $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_SAVE_EXCEPTION, 'error');
        $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
      }
      else {
        // Display exception message to users.
        return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_EXCEPTION_MESSAGE, 'warning');
      }
    }

    // Check total limit.
    if ($this->checkTotalLimit() && empty($this->getWebformSetting('limit_total_unique'))) {
      $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::LIMIT_TOTAL_MESSAGE, 'warning');
      if ($webform->access('submission_update_any')) {
        $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
        return FALSE;
      }
      else {
        return $form;
      }
    }

    // Check user limit.
    if ($this->checkUserLimit() && empty($this->getWebformSetting('limit_user_unique'))) {
      $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::LIMIT_USER_MESSAGE, 'warning');
      if ($webform->access('submission_update_any')) {
        $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
        return FALSE;
      }
      else {
        return $form;
      }
    }

    return FALSE;
  }

  /**
   * Display draft, previous submission, and autofill status messages for this 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.
   */
  protected function displayMessages(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    $webform = $this->getWebform();
    $source_entity = $this->getSourceEntity();
    $account = $this->currentUser();

    // Display test message, except on share page.
    if ($this->isGet() && $this->operation === 'test' && !$this->isSharePage()) {
      $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_TEST, 'warning');

      // Display devel generate link for webform or source entity.
      if ($this->moduleHandler->moduleExists('devel_generate') && $this->currentUser()->hasPermission('administer webform')) {
        $query = ['webform_id' => $webform->id()];
        if ($source_entity) {
          $query += [
            'entity_type' => $source_entity->getEntityTypeId(),
            'entity_id' => $source_entity->id(),
          ];
        }
        $query['destination'] = $this->requestHandler->getUrl($webform, $source_entity, 'webform.results_submissions')->toString();
        $offcanvas = WebformDialogHelper::useOffCanvas();
        $build = [
          '#type' => 'link',
          '#title' => $this->t('Generate %title submissions', ['%title' => $webform->label()]),
          '#url' => Url::fromRoute('devel_generate.webform_submission', [], ['query' => $query]),
          '#attributes' => ($offcanvas) ? WebformDialogHelper::getOffCanvasDialogAttributes(400) : ['class' => ['button']],
        ];
        if ($offcanvas) {
          WebformDialogHelper::attachLibraries($form);
        }
        $this->messenger()->addWarning($this->renderer->renderPlain($build));
      }
    }

    // Display admin only message.
    if ($this->isGet()
      && $this->isRoute('webform.canonical')
      && $this->getRouteMatch()->getRawParameter('webform') === $webform->id()
      && !$this->getWebform()->hasPage()) {
      $this->getMessageManager()->display(WebformMessageManagerInterface::ADMIN_PAGE, 'info');
    }

    // Display loaded or saved draft message.
    if ($webform_submission->isDraft()) {
      if ($form_state->get('draft_saved')) {
        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DRAFT_SAVED_MESSAGE);
        $form_state->set('draft_saved', FALSE);
      }
      elseif ($this->isGet() && !$webform->getSetting('draft_multiple') && !$webform->isClosed()) {
        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DRAFT_LOADED_MESSAGE);
      }
    }

    // Display link to multiple drafts message when user is adding a new
    // submission.
    if ($this->isGet()
      && $this->operation === 'add'
      && $this->getWebformSetting('draft') !== WebformInterface::DRAFT_NONE
      && $this->getWebformSetting('draft_multiple', FALSE)
      && ($previous_draft_total = $this->getStorage()->getTotal($webform, $this->sourceEntity, $this->currentUser(), ['in_draft' => TRUE, 'check_source_entity' => TRUE]))
    ) {
      if ($previous_draft_total > 1) {
        $this->getMessageManager()->display(WebformMessageManagerInterface::DRAFT_PENDING_MULTIPLE);
      }
      else {
        $draft_submission = $this->getStorage()->loadDraft($webform, $this->sourceEntity, $this->currentUser());
        if (!$draft_submission || $webform_submission->id() !== $draft_submission->id()) {
          $this->getMessageManager()->display(WebformMessageManagerInterface::DRAFT_PENDING_SINGLE);
        }
      }
    }

    // Display link to previous submissions message when user is adding a new
    // submission.
    if ($this->isGet()
      && $this->operation === 'add'
      && $this->getWebformSetting('form_previous_submissions', FALSE)
      && ($webform->access('submission_view_own') || $this->currentUser()->hasPermission('view own webform submission'))
      && ($previous_submission_total = $this->getStorage()->getTotal($webform, $this->sourceEntity, $this->currentUser()))
    ) {
      if ($previous_submission_total > 1) {
        $this->getMessageManager()->display(WebformMessageManagerInterface::PREVIOUS_SUBMISSIONS);
      }
      else {
        $last_submission = $this->getStorage()->getLastSubmission($webform, $source_entity, $account);
        if ($last_submission && $webform_submission->id() !== $last_submission->id()) {
          $this->getMessageManager()->display(WebformMessageManagerInterface::PREVIOUS_SUBMISSION);
        }
      }
    }

    // Display autofill message.
    if ($this->isGet()
      && $this->operation === 'add'
      && $webform_submission->isNew()
      && $webform->getSetting('autofill')
      && $this->getStorage()->getLastSubmission($webform, $source_entity, $account, ['in_draft' => FALSE, 'access_check' => FALSE])) {
      $this->getMessageManager()->display(WebformMessageManagerInterface::AUTOFILL_MESSAGE);
    }
  }

  /* ************************************************************************ */
  // Webform libraries and behaviors.
  /* ************************************************************************ */

  /**
   * Attach libraries to the form.
   *
   * @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.
   */
  protected function attachLibraries(array &$form, FormStateInterface $form_state) {
    // Default: Add CSS and JS.
    // @see https://www.drupal.org/node/2274843#inline
    $form['#attached']['library'][] = 'webform/webform.form';

    // Assets: Add custom shared and webform specific CSS and JS.
    // @see webform_library_info_build()
    // @see _webform_page_attachments()
    $webform = $this->getWebform();
    $assets = $webform->getAssets();
    foreach ($assets as $type => $value) {
      if ($value) {
        $form['#attached']['library'][] = 'webform/webform.' . $type . '.' . $webform->id();
      }
    }
  }

  /**
   * Attach behaviors with libraries to the form.
   *
   * @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.
   */
  protected function attachBehaviors(array &$form, FormStateInterface $form_state) {
    // Form: Inline form errors.
    // Add #disable_inline_form_errors property to form.
    if ($this->getWebformSetting('form_disable_inline_errors')) {
      $form['#disable_inline_form_errors'] = TRUE;
    }

    // Form: Novalidate
    // Add novalidate attribute to form if client side validation disabled.
    if ($this->getWebformSetting('form_novalidate')) {
      $form['#attributes']['novalidate'] = 'novalidate';
    }

    // Form: Autocomplete.
    // Add autocomplete=off attribute to form if autocompletion is disabled.
    if ($this->getWebformSetting('form_disable_autocomplete')) {
      $form['#attributes']['autocomplete'] = 'off';
    }

    // Form: Disable back button.
    if ($this->getWebformSetting('form_disable_back')) {
      $form['#attached']['library'][] = 'webform/webform.form.disable_back';
    }

    // Form: Back button submit.
    // Move back when back button is pressed on multistep forms.
    if ($this->getWebformSetting('form_submit_back') && !$this->isAjax()) {
      $form['#attached']['library'][] = 'webform/webform.form.submit_back';
    }

    // Form; Unsaved.
    // Add unsaved message.
    if ($this->getWebformSetting('form_unsaved')) {
      $form['#attributes']['class'][] = 'js-webform-unsaved';
      // Set 'data-webform-unsaved' attribute if unsaved wizard.
      $pages = $this->getPages($form, $form_state);
      $current_page = $this->getCurrentPage($form, $form_state);
      if ($current_page && ($current_page !== $this->getFirstPage($pages))) {
        $form['#attributes']['data-webform-unsaved'] = TRUE;
      }
      $form['#attached']['library'][] = 'webform/webform.form.unsaved';
    }

    // Form: Submit once.
    // Prevent duplicate submissions.
    if ($this->getWebformSetting('form_submit_once')) {
      $form['#attributes']['class'][] = 'js-webform-submit-once';
      $form['#attached']['library'][] = 'webform/webform.form.submit_once';
    }

    // Form: Autosubmit.
    // Disable webform auto submit on enter for wizard webform pages only.
    if ($this->hasPages()) {
      $form['#attributes']['class'][] = 'js-webform-disable-autosubmit';
    }

    // Element: Autofocus.
    // Add autofocus class to webform.
    if ($this->entity->isNew() && $this->getWebformSetting('form_autofocus')) {
      $form['#attributes']['class'][] = 'js-webform-autofocus';
      $form['#attached']['library'][] = 'webform/webform.form.auto_focus';
    }

    // Details: Save.
    // Attach details element save open/close library.
    // This ensures that the library will be loaded even if the webform is
    // used as a block or a node.
    if ($this->config('webform.settings')->get('ui.details_save')) {
      $form['#attached']['library'][] = 'webform/webform.element.details.save';
    }

    // Details Toggle:
    // Display collapse/expand all details link.
    if ($this->getWebformSetting('form_details_toggle')) {
      $form['#attributes']['class'][] = 'js-webform-details-toggle';
      $form['#attributes']['class'][] = 'webform-details-toggle';
      $form['#attached']['library'][] = 'webform/webform.element.details.toggle';
    }
  }

  /* ************************************************************************ */
  // Webform actions.
  /* ************************************************************************ */

  /**
   * {@inheritdoc}
   */
  public function afterBuild(array $form, FormStateInterface $form_state) {
    // If webform has a custom #action remove Form API fields.
    // @see \Drupal\Core\Form\FormBuilder::prepareForm
    if (strpos($form['#action'], 'form_action_') === FALSE) {
      // Remove 'op' #name from all action buttons.
      foreach (Element::children($form['actions']) as $child_key) {
        unset($form['actions'][$child_key]['#name']);
      }
      unset(
        $form['form_build_id'],
        $form['form_token'],
        $form['form_id']
      );
    }
    return $form;
  }

  /**
   * Returns the wizard page submit buttons for the current entity form.
   */
  protected function pagesElement(array $form, FormStateInterface $form_state) {
    $pages = $this->getPages($form, $form_state);
    if (!$pages) {
      return NULL;
    }

    $current_page_name = $this->getCurrentPage($form, $form_state);
    if (!$this->getWebformSetting('wizard_progress_link') && !($this->getWebformSetting('wizard_preview_link') && $current_page_name === WebformInterface::PAGE_PREVIEW)) {
      return NULL;
    }

    $page_indexes = array_flip(array_keys($pages));
    $current_index = $page_indexes[$current_page_name] - 1;

    // Build dedicated actions element for pages links.
    $element = [
      '#type' => 'actions',
      '#weight' => -20,
      '#attributes' => [
        'class' => ['webform-wizard-pages-links', 'js-webform-wizard-pages-links'],
      ],
      // Only process the container and prevent .form-actions from being added
      // which force submit buttons to be rendered in dialogs.
      // @see \Drupal\Core\Render\Element\Actions
      // @see Drupal.behaviors.dialog.prepareDialogButtons
      '#process' => [
        ['\Drupal\Core\Render\Element\Actions', 'processContainer'],
      ],
    ];
    if ($this->getWebformSetting('wizard_progress_link')) {
      $element['#attributes']['data-wizard-progress-link'] = 'true';
    }
    if ($this->getWebformSetting('wizard_preview_link')) {
      $element['#attributes']['data-wizard-preview-link'] = 'true';
    }

    $index = 1;
    $total = count($pages);
    foreach ($pages as $page_name => $page) {
      // Always include submit button for each page but only allows access
      // to previous and visible pages.
      //
      // Developers who want to allow users to jump to any wizard page can
      // expose these buttons via a form alter hook. Beware that
      // skipped pages will not be validated.
      $access = ($page['#access'] && ($page_indexes[$page_name] <= $current_index)) ? TRUE : FALSE;
      $t_args = [
        '@label' => $page['#title'],
        '@start' => $index++,
        '@end' => $total,
      ];
      $element[$page_name] = [
        '#type' => 'submit',
        '#value' => $this->t('Edit'),
        '#page' => $page_name,
        '#validate' => ['::noValidate'],
        '#submit' => ['::gotoPage'],
        '#name' => 'webform_wizard_page-' . $page_name,
        '#attributes' => [
          'data-webform-page' => $page_name,
          'formnovalidate' => 'formnovalidate',
          'class' => ['webform-wizard-pages-link', 'js-webform-wizard-pages-link'],
          'title' => $this->t("Edit '@label' (@start of @end)", $t_args),
        ],
        '#access' => $access,
      ];
    }

    $element['#attached']['library'][] = 'webform/webform.wizard.pages';

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  protected function actionsElement(array $form, FormStateInterface $form_state) {
    // Custom webforms, which completely override the ContentEntityForm, should
    // not return the actions element (aka submit buttons).
    if (!empty($form['#custom_form'])) {
      return NULL;
    }
    $element = parent::actionsElement($form, $form_state);
    if (!empty($element)) {
      $element['#theme'] = 'webform_actions';
    }
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  protected function actions(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->entity;

    $element = parent::actions($form, $form_state);

    $preview_mode = $this->getWebformSetting('preview');

    // Mark the submit action as the primary action, when it appears.
    $element['submit']['#button_type'] = 'primary';
    $element['submit']['#attributes']['class'][] = 'webform-button--submit';
    $element['submit']['#weight'] = 10;

    // Customize the submit button's label for new submissions only.
    if ($webform_submission->isNew() || $webform_submission->isDraft()) {
      $element['submit']['#value'] = $this->config('webform.settings')->get('settings.default_submit_button_label');
    }

    // Add validate and complete handler to submit.
    $element['submit']['#validate'][] = '::validateForm';
    $element['submit']['#validate'][] = '::autosave';
    $element['submit']['#validate'][] = '::complete';

    // Add confirm(ation) handler to submit button.
    $element['submit']['#submit'][] = '::confirmForm';

    // Hide the delete button and move it last.
    if (isset($element['delete'])) {
      $element['delete']['#access'] = FALSE;
      $element['delete']['#title'] = $this->config('webform.settings')->get('settings.default_delete_button_label');
      // Redirect to the 'add' submission form when this submission is deleted.
      if ($this->operation === 'add') {
        $element['delete']['#url']->mergeOptions(['query' => $this->getRedirectDestination()->getAsArray()]);
      }
      $element['delete']['#weight'] = 20;
    }

    $pages = $this->getPages($form, $form_state);
    $current_page = $this->getCurrentPage($form, $form_state);
    if ($pages) {
      // Get current page element which can contain custom prev(ious) and next button
      // labels.
      $current_page_element = $this->getWebform()->getPage($this->operation, $current_page);
      $previous_page = $this->getPreviousPage($pages, $current_page);
      $next_page = $this->getNextPage($pages, $current_page);

      // Track previous and next page.
      $track = $this->getWebform()->getSetting('wizard_track');
      switch ($track) {
        case 'index':
          $track_pages = array_flip(array_keys($pages));
          $track_previous_page = ($previous_page) ? $track_pages[$previous_page] + 1 : NULL;
          $track_next_page = ($next_page) ? $track_pages[$next_page] + 1 : NULL;
          $track_last_page = ($this->getWebform()->getSetting('wizard_confirmation')) ? count($track_pages) : count($track_pages) + 1;
          break;

        default;
        case 'name':
          $track_previous_page = $previous_page;
          $track_next_page = $next_page;
          $track_last_page = WebformInterface::PAGE_CONFIRMATION;
          break;
      }

      $is_first_page = ($current_page === $this->getFirstPage($pages)) ? TRUE : FALSE;
      $is_last_page = (in_array($current_page, [WebformInterface::PAGE_PREVIEW, WebformInterface::PAGE_CONFIRMATION, $this->getLastPage($pages)])) ? TRUE : FALSE;
      $is_preview_page = ($current_page === WebformInterface::PAGE_PREVIEW);
      $is_next_page_preview = ($next_page === WebformInterface::PAGE_PREVIEW) ? TRUE : FALSE;
      $is_next_page_complete = ($next_page === WebformInterface::PAGE_CONFIRMATION) ? TRUE : FALSE;
      $is_next_page_optional_preview = ($is_next_page_preview && $preview_mode !== DRUPAL_REQUIRED);

      // Only show that save button if this is the last page of the wizard or
      // on preview page or right before the optional preview.
      $element['submit']['#access'] = $is_last_page || $is_preview_page || $is_next_page_optional_preview || $is_next_page_complete;

      // Use next page submit callback to make sure conditional page logic
      // is executed.
      $element['submit']['#submit'] = ['::submit'];

      if ($track) {
        $element['submit']['#attributes']['data-webform-wizard-page'] = $track_last_page;
      }

      if (!$is_first_page) {
        if ($is_preview_page) {
          $element['preview_prev'] = [
            '#type' => 'submit',
            '#value' => $this->config('webform.settings')->get('settings.default_preview_prev_button_label'),
            // @see \Drupal\webform\WebformSubmissionForm::noValidate
            '#validate' => ['::noValidate'],
            '#submit' => ['::previous'],
            '#attributes' => [
              'formnovalidate' => 'formnovalidate',
              'class' => ['webform-button--previous'],
            ],
            '#weight' => 0,
          ];
          if ($track) {
            $element['preview_prev']['#attributes']['data-webform-wizard-page'] = $track_previous_page;
          }
        }
        else {
          if (isset($current_page_element['#prev_button_label'])) {
            $previous_button_label = $current_page_element['#prev_button_label'];
            $previous_button_custom = TRUE;
          }
          else {
            $previous_button_label = $this->config('webform.settings')->get('settings.default_wizard_prev_button_label');
            $previous_button_custom = FALSE;
          }
          $element['wizard_prev'] = [
            '#type' => 'submit',
            '#value' => $previous_button_label,
            '#webform_actions_button_custom' => $previous_button_custom,
            // @see \Drupal\webform\WebformSubmissionForm::noValidate
            '#validate' => ['::noValidate'],
            '#submit' => ['::previous'],
            '#attributes' => [
              'formnovalidate' => 'formnovalidate',
              'class' => ['webform-button--previous'],
            ],
            '#weight' => 0,
          ];
          if ($track) {
            $element['wizard_prev']['#attributes']['data-webform-wizard-page'] = $track_previous_page;
          }
        }
      }

      if (!$is_last_page && !$is_next_page_complete) {
        if ($is_next_page_preview) {
          $element['preview_next'] = [
            '#type' => 'submit',
            '#value' => $this->config('webform.settings')->get('settings.default_preview_next_button_label'),
            '#validate' => ['::validateForm'],
            '#submit' => ['::next'],
            '#attributes' => ['class' => ['webform-button--preview']],
            '#weight' => 1,
          ];
          if ($track) {
            $element['preview_next']['#attributes']['data-webform-wizard-page'] = $track_next_page;
          }
        }
        else {
          if (isset($current_page_element['#next_button_label'])) {
            $next_button_label = $current_page_element['#next_button_label'];
            $next_button_custom = TRUE;
          }
          else {
            $next_button_label = $this->config('webform.settings')->get('settings.default_wizard_next_button_label');
            $next_button_custom = FALSE;
          }
          $element['wizard_next'] = [
            '#type' => 'submit',
            '#value' => $next_button_label,
            '#webform_actions_button_custom' => $next_button_custom,
            '#validate' => ['::validateForm'],
            '#submit' => ['::next'],
            '#attributes' => ['class' => ['webform-button--next']],
            '#weight' => 1,
          ];
          if ($track) {
            $element['wizard_next']['#attributes']['data-webform-wizard-page'] = $track_next_page;
          }
        }
      }
      if ($track) {
        $element['#attached']['library'][] = 'webform/webform.wizard.track';
      }
    }

    // Draft.
    if ($this->draftEnabled()) {
      $element['draft'] = [
        '#type' => 'submit',
        '#value' => $this->config('webform.settings')->get('settings.default_draft_button_label'),
        '#validate' => ['::draft'],
        '#submit' => ['::submitForm', '::save', '::rebuild'],
        '#attributes' => [
          'formnovalidate' => 'formnovalidate',
          'class' => ['webform-button--draft'],
        ],
        '#weight' => -10,
      ];
    }

    // Reset.
    if ($this->resetEnabled()) {
      $element['reset'] = [
        '#type' => 'submit',
        '#value' => $this->config('webform.settings')->get('settings.default_reset_button_label'),
        '#validate' => ['::noValidate'],
        '#submit' => ['::reset'],
        '#attributes' => [
          'formnovalidate' => 'formnovalidate',
          'class' => ['webform-button--reset'],
        ],
        '#weight' => 10,
      ];
    }

    uasort($element, ['Drupal\Component\Utility\SortArray', 'sortByWeightProperty']);

    return $element;
  }

  /**
   * Webform submission handler for the 'goto' action.
   *
   * @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.
   */
  public function gotoPage(array &$form, FormStateInterface $form_state) {
    $element = $form_state->getTriggeringElement();
    $form_state->set('current_page', $element['#page']);
    $this->wizardSubmit($form, $form_state);
  }

  /**
   * Webform submission handler for the 'submit' action.
   *
   * @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.
   */
  public function submit(array &$form, FormStateInterface $form_state) {
    $this->next($form, $form_state, TRUE);
  }

  /**
   * Webform submission handler for the 'next' action.
   *
   * @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.
   * @param bool $skip_preview
   *   Skips the preview page.
   */
  public function next(array &$form, FormStateInterface $form_state, $skip_preview = FALSE) {
    if ($form_state->getErrors()) {
      return;
    }
    $pages = $this->getPages($form, $form_state);

    // Get next page.
    $current_page = $this->getCurrentPage($form, $form_state);
    $next_page = $this->getNextPage($pages, $current_page);

    // If there is no next page jump to the confirmation page which will also
    // submit this form.
    // @see \Drupal\webform\WebformSubmissionForm::wizardSubmit
    if (empty($next_page)) {
      $next_page = WebformInterface::PAGE_CONFIRMATION;
    }

    // Skip preview page and move to the confirmation page.
    // @see
    if ($skip_preview && $next_page === WebformInterface::PAGE_PREVIEW) {
      $next_page = WebformInterface::PAGE_CONFIRMATION;
    }

    // Set next page.
    $form_state->set('current_page', $next_page);

    // Submit next page.
    $this->wizardSubmit($form, $form_state);
  }

  /**
   * Webform submission handler for the 'previous' action.
   *
   * @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.
   */
  public function previous(array &$form, FormStateInterface $form_state) {
    $pages = $this->getPages($form, $form_state);

    // Get previous page.
    $current_page = $this->getCurrentPage($form, $form_state);
    $previous_page = $this->getPreviousPage($pages, $current_page);

    // Set previous page.
    $form_state->set('current_page', $previous_page);

    // Submit previous page.
    $this->wizardSubmit($form, $form_state);
  }

  /**
   * Webform submission handler for the wizard submit action.
   *
   * @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.
   */
  protected function wizardSubmit(array &$form, FormStateInterface $form_state) {
    $current_page = $form_state->get('current_page');

    if ($current_page === WebformInterface::PAGE_CONFIRMATION) {
      $this->complete($form, $form_state);
      $this->submitForm($form, $form_state);
      $this->save($form, $form_state);
      $this->confirmForm($form, $form_state);
    }
    elseif ($this->draftEnabled() && $this->getWebformSetting('draft_auto_save') && !$this->entity->isCompleted()) {
      $form_state->set('in_draft', TRUE);

      $this->submitForm($form, $form_state);
      $this->save($form, $form_state);
      $this->rebuild($form, $form_state);
    }
    else {
      $this->submitForm($form, $form_state);
      $this->rebuild($form, $form_state);
    }

    // Announce current page with progress.
    // @see template_preprocess_webform_progress()
    if ($this->isAjax()) {
      $pages = $this->getPages($form, $form_state);
      // Make sure the current page exists because the confirmation page
      // may not be included in the wizard's pages.
      if (isset($pages[$current_page])) {
        $page_keys = array_keys($pages);
        $page_indexes = array_flip($page_keys);
        $total_pages = count($page_keys);
        $current_index = $page_indexes[$current_page];

        $t_args = [
          '@title' => $this->getWebform()->label(),
          '@page' => $pages[$current_page]['#title'],
          '@start' => ($current_index + 1),
          '@end' => $total_pages,
        ];
        $this->announce($this->t('"@title: @page" loaded. (@start of @end)', $t_args));
      }
    }
  }

  /**
   * Webform submission handler to autosave when there are validation errors.
   *
   * @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.
   */
  public function autosave(array &$form, FormStateInterface $form_state) {
    // Make sure the submission exists before validating it.
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    if ($webform_submission->id() && !WebformSubmission::load($webform_submission->id())) {
      return;
    }

    if ($form_state->hasAnyErrors()) {
      if ($this->draftEnabled() && $this->getWebformSetting('draft_auto_save') && !$this->entity->isCompleted()) {
        $form_state->set('in_draft', TRUE);

        $was_new = $this->entity->isNew();

        $this->submitForm($form, $form_state);
        $this->save($form, $form_state);
        $this->rebuild($form, $form_state);

        if ($was_new && $form_state->hasAnyErrors()) {
          // Prevent the previously-cached form object, which is stored in
          // $form['build_info']['callback_object'] from being used because it
          // refers to the original new (unsaved) entity.
          $this->formBuilder->deleteCache($form['#build_id']);
        }
      }
    }
  }

  /**
   * Webform submission handler for the 'draft' action.
   *
   * @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.
   */
  public function draft(array &$form, FormStateInterface $form_state) {
    $form_state->clearErrors();
    $form_state->set('in_draft', TRUE);
    $form_state->set('draft_saved', TRUE);
    $this->entity->validate();
  }

  /**
   * Webform submission handler for the 'complete' action.
   *
   * @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.
   */
  public function complete(array &$form, FormStateInterface $form_state) {
    $form_state->set('in_draft', FALSE);
  }

  /**
   * Webform submission validation that does nothing but clear validation errors.
   *
   * This method is used by wizard/preview previous buttons and the reset button
   * to prevent all form validation errors from being displayed while still
   * allowing an element's #validate callback to be triggered.
   *
   * This callback is being used instead of adding
   * #limit_validation_errors = [] to the submit buttons because
   * #limit_validation_errors also ignores all form values set via an element's
   * #validate callback.
   *
   * More complex (web)form elements user #validate callbacks
   * to process and alter an element's submitted value. Element's that rely on
   * #validate to alter the submitted value include 'Password Confirm',
   * 'Email Confirm', 'Composite Elements', 'Other Elements', and more…
   *
   * If the #limit_validation_errors property is used within a multi-step wizard
   * form, previously submitted values will be corrupted.
   *
   * @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.
   *
   * @see \Drupal\Core\Form\FormValidator::handleErrorsWithLimitedValidation
   * @see \Drupal\Core\Render\Element\PasswordConfirm::validatePasswordConfirm
   * @see \Drupal\webform\Element\WebformEmailConfirm
   * @see \Drupal\webform\Element\WebformOtherBase::validateWebformOther
   */
  public function noValidate(array &$form, FormStateInterface $form_state) {
    $form_state->clearErrors();
    $this->entity->validate();
  }

  /**
   * Webform submission handler for the 'rebuild' action.
   *
   * @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.
   */
  public function rebuild(array &$form, FormStateInterface $form_state) {
    $form_state->setRebuild();
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Make sure the submission exists before validating it.
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    if ($webform_submission->id() && !WebformSubmission::load($webform_submission->id())) {
      $form_state->setErrorByName(NULL, $this->t('An error occurred while trying to validate the submission. Please save your work and reload this page.'));
      return;
    }

    parent::validateForm($form, $form_state);

    // Disable inline form error when performing validation via the API.
    if ($this->operation === 'api') {
      // @see \Drupal\webform\WebformSubmissionForm::submitWebformSubmission
      $form['#disable_inline_form_errors'] = TRUE;
    }

    // Build webform submission with validated and processed data.
    $this->entity = $this->buildEntity($form, $form_state);

    // Server side #states API validation.
    $this->conditionsValidator->validateForm($form, $form_state);

    // Validate webform via webform handler.
    $this->getWebform()->invokeHandlers('validateForm', $form, $form_state, $this->entity);

    // Webform validate handlers (via form['#validate']) are not called when
    // #validate handlers are attached to the trigger element
    // (i.e. submit button), so we need to manually call $form['validate']
    // handlers to support the modules that use form['#validate'] like the
    // validators.module.
    // @see \Drupal\webform\WebformSubmissionForm::actions
    // @see \Drupal\Core\Form\FormBuilder::doBuildForm
    $trigger_element = $form_state->getTriggeringElement();
    if (isset($trigger_element['#validate'])) {
      $handlers = array_filter($form['#validate'], function ($callback) {
        // Remove ::validateForm to prevent a recursion.
        return (is_array($callback) || $callback !== '::validateForm');
      });
      // @see \Drupal\Core\Form\FormValidator::executeValidateHandlers
      foreach ($handlers as $callback) {
        $arguments = [&$form, &$form_state];
        call_user_func_array($form_state->prepareCallback($callback), $arguments);
      }
    }

    // Validate file (upload) limit.
    $this->validateUploadedManagedFiles($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    parent::submitForm($form, $form_state);

    // Server side #states API submit.
    $this->conditionsValidator->submitForm($form, $form_state);

    // Submit webform via webform handler.
    $this->getWebform()->invokeHandlers('submitForm', $form, $form_state, $this->entity);
  }

  /**
   * Webform confirm(ation) handler.
   *
   * @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.
   */
  public function confirmForm(array &$form, FormStateInterface $form_state) {
    $this->setConfirmation($form_state);

    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();

    // Confirm webform via webform handler.
    $this->getWebform()->invokeHandlers('confirmForm', $form, $form_state, $webform_submission);

    // Get confirmation type and submission state.
    $confirmation_type = $this->getWebformSetting('confirmation_type');
    $state = $webform_submission->getState();

    // Rebuild or reset the form if reloading the current form via AJAX.
    if ($this->isAjax()) {
      // Track that Ajax is restarting the form so that the submission
      // can be prepopulated.
      // @see \Drupal\webform\WebformSubmissionForm::buildForm
      $form_state->set('is_ajax_restart', TRUE);

      // On update, rebuild and display message unless ?destination= is set.
      // @see \Drupal\webform\WebformSubmissionForm::setConfirmation
      if ($state === WebformSubmissionInterface::STATE_UPDATED) {
        if (!$this->getRequest()->get('destination')) {
          $this->rebuild($form, $form_state);
        }
      }
      elseif ($confirmation_type === WebformInterface::CONFIRMATION_MESSAGE || $confirmation_type === WebformInterface::CONFIRMATION_NONE) {
        $this->reset($form, $form_state);
      }
    }

    // Always rebuild or  reset the form to trigger a modal dialog.
    if ($confirmation_type === WebformInterface::CONFIRMATION_MODAL) {
      if ($state === WebformSubmissionInterface::STATE_UPDATED) {
        $this->rebuild($form, $form_state);
      }
      else {
        $this->reset($form, $form_state);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    $webform = $this->getWebform();
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();

    // Apply variants.
    $webform->applyVariants($webform_submission);

    // Make sure the uri and remote addr are set correctly because
    // Ajax requests can cause these values to be reset.
    if ($webform_submission->isNew()) {
      if (preg_match('/\.webform\.test_form$/', $this->getRouteMatch()->getRouteName())) {
        // For test submissions use the source URL.
        $source_url = $webform_submission->set('uri', NULL)->getSourceUrl()->setAbsolute(FALSE);
        $uri = preg_replace('#^' . base_path() . '#', '/', $source_url->toString());
      }
      else {
        // For all other submissions, use the request URI.
        $uri = preg_replace('#^' . base_path() . '#', '/', $this->getRequest()->getRequestUri());
        // Remove Ajax query string parameters.
        $uri = preg_replace('/(ajax_form=1|_wrapper_format=(drupal_ajax|drupal_modal|drupal_dialog|html|ajax))(&|$)/', '', $uri);
        // Remove empty query string.
        $uri = preg_replace('/\?$/', '', $uri);
      }
      $webform_submission->set('uri', $uri);
      $webform_submission->set('remote_addr', ($this->getWebform()->hasRemoteAddr()) ? $this->getRequest()->getClientIp() : '');
      if ($this->isConfidential()) {
        $webform_submission->setOwnerId(0);
      }
    }

    // Block users from submitting templates that they can't update.
    if ($webform->isTemplate() && !$webform->access('update')) {
      return;
    }

    // Save and log webform submission.
    $webform_submission->save();

    // Invalidate cache if any limits are specified.
    if ($this->getWebformSetting('limit_total')
      || $this->getWebformSetting('user_limit_total')
      || $this->getWebformSetting('entity_limit_total')
      || $this->getWebformSetting('entity_limit_user')
      || $this->getWebformSetting('limit_total_unique')
      || $this->getWebformSetting('limit_user_unique')
    ) {
      Cache::invalidateTags(['webform:' . $this->getWebform()->id()]);
    }

    // Check limits rebuild.
    if ($this->checkTotalLimit() || $this->checkUserLimit()) {
      $form_state->setRebuild();
    }
  }

  /**
   * Webform submission handler for the 'reset' action.
   *
   * @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.
   */
  public function reset(array &$form, FormStateInterface $form_state) {
    // Delete save draft.
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    if ($webform_submission->isDraft()) {
      $webform_submission->delete();
    }

    // Create new webform submission.
    /** @var \Drupal\webform\Entity\WebformSubmission $webform_submission */
    $webform_submission = $this->getEntity()->createDuplicate();
    $webform_submission->setData($this->originalData);
    $this->setEntity($webform_submission);

    // Reset user input but preserve form tokens.
    $form_state->setUserInput(array_intersect_key($form_state->getUserInput(), [
      'form_build_id' => 'form_build_id',
      'form_token' => 'form_token',
      'form_id' => 'form_id',
    ]));

    // Reset values.
    $form_state->setValues([]);

    // Reset current page.
    $storage = $form_state->getStorage();
    unset($storage['current_page']);
    $form_state->setStorage($storage);

    // Rebuild the form.
    $this->rebuild($form, $form_state);
  }

  /* ************************************************************************ */
  // Validate functions.
  /* ************************************************************************ */

  /**
   * Validate uploaded managed file limits.
   *
   * @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.
   *
   * @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::validateManagedFileLimit
   */
  protected function validateUploadedManagedFiles(array $form, FormStateInterface $form_state) {
    $file_limit = $this->getWebform()->getSetting('form_file_limit')
      ?: $this->configFactory->get('webform.settings')->get('settings.default_form_file_limit')
      ?: '';
    $file_limit = Bytes::toNumber($file_limit);
    if (!$file_limit) {
      return;
    }

    // Validate file upload limit.
    $fids = $this->getUploadedManagedFileIds();
    if (!$fids) {
      return;
    }
    $file_names = [];
    $total_file_size = 0;

    /** @var \Drupal\file\FileInterface[] $files */
    $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
    foreach ($files as $file) {
      $total_file_size += (int) $file->getSize();
      $file_names[] = $file->getFilename() . ' - ' . format_size($file->getSize(), $this->entity->language()->getId());
    }

    if ($total_file_size > $file_limit) {
      $t_args = ['%quota' => format_size($file_limit)];
      $message = [];
      $message['content'] = ['#markup' => $this->t("This form's file upload quota of %quota has been exceeded. Please remove some files.", $t_args)];
      $message['files'] = [
        '#theme' => 'item_list',
        '#items' => $file_names,
      ];
      $form_state->setErrorByName(NULL, $this->renderer->renderPlain($message));
    }
  }

  /**
   * Get uploaded managed file ids.
   *
   * @return array
   *   An array of uploaded file ids.
   */
  protected function getUploadedManagedFileIds() {
    $fids = [];

    $element_keys = $this->getWebform()->getElementsManagedFiles();
    foreach ($element_keys as $element_key) {
      $data = $this->entity->getElementData($element_key);
      if (!$data) {
        continue;
      }

      $element = $this->getWebform()->getElement($element_key);
      $element_plugin = $this->elementManager->getElementInstance($element);
      $multiple = $element_plugin->hasMultipleValues($element);

      // Get fids from composite sub-elements.
      if ($element_plugin instanceof WebformCompositeBase) {
        $managed_file_keys = $element_plugin->getManagedFiles($element);
        // Convert single composite value to array of multiple composite values.
        $data = (!$multiple) ? [$data] : $data;
        foreach ($data as $item) {
          foreach ($managed_file_keys as $manage_file_key) {
            if ($item[$manage_file_key]) {
              $fids[] = $item[$manage_file_key];
            }
          }
        }
      }
      else {
        $fids = array_merge($fids, (array) $data);
      }
    }

    return $fids;
  }

  /* ************************************************************************ */
  // Webform functions.
  /* ************************************************************************ */

  /**
   * Set the webform properties from the elements.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param array $elements
   *   An associative array containing the elements.
   */
  protected function setFormPropertiesFromElements(array &$form, array &$elements) {
    foreach ($elements as $key => $value) {
      if (is_string($key) && $key[0] === '#') {
        $value = $this->tokenManager->replace($value, $this->getEntity(), [], [], $this->bubbleableMetadata);
        if (isset($form[$key]) && is_array($form[$key]) && is_array($value)) {
          $form[$key] = NestedArray::mergeDeep($form[$key], $value);
        }
        else {
          $form[$key] = $value;
        }
        // Remove the properties from the $elements and $form['elements'] array.
        unset(
          $elements[$key],
          $form['elements'][$key]
        );
      }
    }
    // Replace token in #attributes.
    if (isset($form['#attributes'])) {
      $form['#attributes'] = $this->tokenManager->replace($form['#attributes'], $this->getEntity(), [], [], $this->bubbleableMetadata);
    }
  }

  /* ************************************************************************ */
  // Wizard page functions.
  /* ************************************************************************ */

  /**
   * Determine if this is a multi-step wizard form.
   *
   * @return bool
   *   TRUE if this multi-step wizard form.
   */
  protected function hasPages() {
    return $this->getWebform()->getPages($this->operation);
  }

  /**
   * Get visible wizard pages.
   *
   * Note: The array of pages is stored in the webform's state so that it can be
   * altered using hook_form_alter() and #validate callbacks.
   *
   * @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.
   *
   * @return array
   *   Array of visible wizard pages.
   */
  protected function getPages(array &$form, FormStateInterface $form_state) {
    if ($form_state->get('pages') === NULL) {
      $pages = $this->getWebform()->getPages($this->operation);
      $form_state->set('pages', $pages);
    }

    // Get pages from form state.
    $pages = $form_state->get('pages');

    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    return $this->conditionsValidator->buildPages($pages, $webform_submission);
  }

  /**
   * Get the current page's key.
   *
   * @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.
   *
   * @return string
   *   The current page's key.
   */
  protected function getCurrentPage(array &$form, FormStateInterface $form_state) {
    if ($form_state->get('current_page') === NULL) {
      $pages = $this->getWebform()->getPages($this->operation, $this->entity);
      if (empty($pages)) {
        $form_state->set('current_page', '');
      }
      else {
        $current_page = $this->entity->getCurrentPage();
        if ($current_page && isset($pages[$current_page]) && !$this->entity->isCompleted()) {
          $form_state->set('current_page', $current_page);
        }
        else {
          $form_state->set('current_page', WebformArrayHelper::getFirstKey($pages));
        }
      }
    }
    return $form_state->get('current_page');
  }

  /**
   * Get first page's key.
   *
   * @param array $pages
   *   An associative array of visible wizard pages.
   *
   * @return null|string
   *   The first page's key.
   */
  protected function getFirstPage(array $pages) {
    return WebformArrayHelper::getFirstKey($pages);
  }

  /**
   * Get last page's key.
   *
   * @param array $pages
   *   An associative array of visible wizard pages.
   *
   * @return null|string
   *   The last page's key.
   */
  protected function getLastPage(array $pages) {
    return WebformArrayHelper::getLastKey($pages);
  }

  /**
   * Get next page's key.
   *
   * @param array $pages
   *   An associative array of visible wizard pages.
   * @param string $current_page
   *   The current page.
   *
   * @return null|string
   *   The next page's key. NULL if there is no next page.
   */
  protected function getNextPage(array $pages, $current_page) {
    return WebformArrayHelper::getNextKey($pages, $current_page);
  }

  /**
   * Get previous page's key.
   *
   * @param array $pages
   *   An associative array of visible wizard pages.
   * @param string $current_page
   *   The current page.
   *
   * @return null|string
   *   The previous page's key. NULL if there is no previous page.
   */
  protected function getPreviousPage(array $pages, $current_page) {
    return WebformArrayHelper::getPreviousKey($pages, $current_page);
  }

  /**
   * Set webform wizard current page.
   *
   * @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.
   */
  protected function displayCurrentPage(array &$form, FormStateInterface $form_state) {
    $current_page = $this->getCurrentPage($form, $form_state);
    if ($current_page === WebformInterface::PAGE_PREVIEW) {
      // Hide all elements except 'webform_actions'.
      foreach ($form['elements'] as $element_key => $element) {
        if (isset($element['#type']) && $element['#type'] === 'webform_actions') {
          continue;
        }

        // Set #access to FALSE which will suppresses webform #required validation.
        if (Element::child($element_key) && is_array($form['elements'])) {
          WebformElementHelper::setPropertyRecursive($form['elements'][$element_key], '#access', FALSE);
        }
      }

      // Display preview message.
      $this->getMessageManager()->display(WebformMessageManagerInterface::FORM_PREVIEW_MESSAGE, 'warning');

      // Build preview.
      $preview_attributes = new Attribute($this->getWebform()->getSetting('preview_attributes'));
      $preview_attributes->addClass('webform-preview');
      $form['#title'] = PlainTextOutput::renderFromHtml($this->getWebformSetting('preview_title'));
      $form['preview'] = [
        '#type' => $this->getWebformSetting('wizard_page_type', 'container'),
        '#title' => $this->getWebformSetting('preview_label'),
        '#title_tag' => $this->getWebformSetting('wizard_page_title_tag', ''),
        '#attributes' => $preview_attributes,
        // Progress bar is -20.
        '#weight' => -10,
        'submission' => $this->entityTypeManager
          ->getViewBuilder('webform_submission')
          ->view($this->entity, 'preview'),
      ];
    }
    else {
      // Get all pages so that we can also hide skipped pages.
      $pages = $this->getWebform()->getPages($this->operation);
      foreach ($pages as $page_key => $page) {
        if (isset($form['elements'][$page_key])) {
          $page_element = &$form['elements'][$page_key];
          $page_element_plugin = $this->elementManager->getElementInstance($page_element);
          if ($page_element_plugin instanceof WebformElementWizardPageInterface) {
            if ($page_key != $current_page) {
              $page_element_plugin->hidePage($page_element);
            }
            else {
              $page_element_plugin->showPage($page_element);
            }
          }
        }
      }
    }
  }

  /* ************************************************************************ */
  // Webform state functions.
  /* ************************************************************************ */

  /**
   * Set webform state to redirect to a trusted redirect response.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param \Drupal\Core\Url $url
   *   A URL object.
   */
  protected function setTrustedRedirectUrl(FormStateInterface $form_state, Url $url) {
    $form_state->setResponse(new TrustedRedirectResponse($url->toString()));
  }

  /**
   * Set webform state confirmation redirect and message.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  protected function setConfirmation(FormStateInterface $form_state) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    $webform = $webform_submission->getWebform();

    // Get current route name, parameters, and options.
    $route_name = $this->getRouteMatch()->getRouteName();
    $route_parameters = $this->getRouteMatch()->getRawParameters()->all();
    $route_options = [];

    // Add current query to route options.
    if (!$webform->getSetting('confirmation_exclude_query')) {
      $query = $this->getRequest()->query->all();
      // Remove Ajax parameters from query.
      unset($query['ajax_form'], $query['_wrapper_format']);
      if ($query) {
        $route_options['query'] = $query;
      }
    }

    // Default to displaying a confirmation message on this page when submission
    // is updated or locked (but not just completed).
    $state = $webform_submission->getState();
    $is_updated = ($state === WebformSubmissionInterface::STATE_UPDATED);
    $is_locked = ($state === WebformSubmissionInterface::STATE_LOCKED && $webform_submission->getChangedTime() > $webform_submission->getCompletedTime());
    $confirmation_update = $this->getWebformSetting('confirmation_update');

    if (($is_updated && !$confirmation_update) || $is_locked) {
      $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_UPDATED);
      $form_state->set('current_page', NULL);
      $form_state->setRedirect($route_name, $route_parameters, $route_options);
      return;
    }

    // Add token route query options.
    if ($state === WebformSubmissionInterface::STATE_COMPLETED && !$webform->getSetting('confirmation_exclude_token')) {
      $route_options['query']['token'] = $webform_submission->getToken();
    }

    // Handle 'page', 'url', and 'inline' confirmation types.
    $confirmation_type = $this->getWebformSetting('confirmation_type');
    switch ($confirmation_type) {
      case WebformInterface::CONFIRMATION_PAGE:
        $redirect_url = $this->requestHandler->getUrl($webform, $this->sourceEntity, 'webform.confirmation', $route_options);
        $form_state->setRedirectUrl($redirect_url);
        return;

      case WebformInterface::CONFIRMATION_URL:
      case WebformInterface::CONFIRMATION_URL_MESSAGE:
        $redirect_url = $this->getConfirmationUrl();
        if ($redirect_url) {
          if ($confirmation_type === WebformInterface::CONFIRMATION_URL_MESSAGE) {
            $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
          }
          $redirect_url->mergeOptions($route_options);
          $this->setTrustedRedirectUrl($form_state, $redirect_url);
        }
        else {
          $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
          $route_options['query']['webform_id'] = $webform->id();
          $form_state->setRedirect($route_name, $route_parameters, $route_options);
        }
        return;

      case WebformInterface::CONFIRMATION_INLINE:
        $form_state->set('current_page', WebformInterface::PAGE_CONFIRMATION);
        $form_state->setRebuild();
        return;

      case WebformInterface::CONFIRMATION_MESSAGE:
        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
        return;

      case WebformInterface::CONFIRMATION_MODAL:
        $message = $this->getMessageManager()->build(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
        if ($message) {
          // Set webform confirmation modal in $form_state.
          $form_state->set('webform_confirmation_modal', [
            'title' => $this->getWebformSetting('confirmation_title', ''),
            'content' => $message,
          ]);
        }
        return;

      case WebformInterface::CONFIRMATION_NONE:
        return;

      case WebformInterface::CONFIRMATION_DEFAULT:
      default:
        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DEFAULT_CONFIRMATION);
        return;
    }
  }

  /**
   * Get the webform's confirmation URL.
   *
   * @return \Drupal\Core\Url|false
   *   The url object, or FALSE if the path is not valid.
   *
   * @see \Drupal\Core\Path\PathValidatorInterface::getUrlIfValid
   */
  protected function getConfirmationUrl() {
    $confirmation_url = trim($this->getWebformSetting('confirmation_url', ''));

    if (strpos($confirmation_url, '/') === 0) {
      // Get redirect URL using an absolute URL for the absolute  path.
      $redirect_url = Url::fromUri($this->getRequest()->getSchemeAndHttpHost() . $confirmation_url);
    }
    elseif (preg_match('#^[a-z]+(?:://|:)#', $confirmation_url)) {
      // Get redirect URL from URI (i.e. http://, https:// or ftp://)
      // and Drupal custom URIs (i.e internal:).
      $redirect_url = Url::fromUri($confirmation_url);
    }
    elseif (strpos($confirmation_url, '<') === 0) {
      // Get redirect URL from special paths: '<front>' and '<none>'.
      $redirect_url = $this->pathValidator->getUrlIfValid($confirmation_url);
    }
    else {
      // Get redirect URL by validating the Drupal relative path which does not
      // begin with a forward slash (/).
      $confirmation_url = $this->aliasManager->getPathByAlias('/' . $confirmation_url);
      $redirect_url = $this->pathValidator->getUrlIfValid($confirmation_url);
    }

    // If redirect url is FALSE, display and log a warning.
    if (!$redirect_url) {
      $webform = $this->getWebform();
      $t_args = [
        '@webform' => $webform->label(),
        '%url' => $this->getWebformSetting('confirmation_url'),
      ];
      // Display warning to use who can update the webform.
      if ($webform->access('update')) {
        $this->messenger()->addWarning($this->t('Confirmation URL %url is not valid.', $t_args));
      }
      // Log warning.
      $this->getLogger('webform')->warning('@webform: Confirmation URL %url is not valid.', $t_args);
    }

    return $redirect_url;
  }

  /**
   * Hide confirmation modal during form validation.
   *
   * This prevent duplicate modal dialog from appearing.
   */
  public static function removeConfirmationModal(&$element, FormStateInterface $form_state, &$complete_form) {
    // Reset confirmation modal.
    $storage = $form_state->getStorage();
    unset($storage['webform_confirmation_modal']);
    $form_state->setStorage($storage);

    // Remove modal from form.
    unset($complete_form['webform_confirmation_modal']);
  }

  /* ************************************************************************ */
  // Elements functions.
  /* ************************************************************************ */

  /**
   * Prepare webform elements.
   *
   * @param array $elements
   *   An render array representing elements.
   * @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.
   */
  protected function prepareElements(array &$elements, array &$form, FormStateInterface $form_state) {
    foreach ($elements as $key => &$element) {
      if (!WebformElementHelper::isElement($element, $key)) {
        continue;
      }

      // Build the webform element.
      $this->elementManager->buildElement($element, $form, $form_state);

      if (isset($element['#states'])) {
        $element['#states'] = $this->addStatesPrefix($element['#states']);
      }

      // Recurse and prepare nested elements.
      $this->prepareElements($element, $form, $form_state);
    }
  }

  /**
   * Alter webform elements form.
   *
   * @param array $elements
   *   An render array representing elements.
   * @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.
   */
  protected function alterElementsForm(array &$elements, array &$form, FormStateInterface $form_state) {
    foreach ($elements as $key => &$element) {
      if (!WebformElementHelper::isElement($element, $key)) {
        continue;
      }

      $element_plugin = $this->elementManager->getElementInstance($element);
      if ($element_plugin) {
        $element_plugin->alterForm($element, $form, $form_state);
      }

      // Recurse and alter nested elements forms.
      $this->alterElementsForm($element, $form, $form_state);
    }
  }

  /**
   * Add unique class prefix to all :input #states selectors.
   *
   * @param array $array
   *   An associative array.
   *
   * @return array
   *   An associative array with unique class prefix added to all :input
   *   #states selectors.
   */
  protected function addStatesPrefix(array $array) {
    $prefixed_array = [];
    foreach ($array as $key => $value) {
      if (strpos($key, ':input') === 0) {
        $key = $this->statesPrefix . ' ' . $key;
        $prefixed_array[$key] = $value;
      }
      elseif (is_array($value)) {
        $prefixed_array[$key] = $this->addStatesPrefix($value);
      }
      else {
        $prefixed_array[$key] = $value;
      }
    }
    return $prefixed_array;
  }

  /**
   * Prepopulate element data.
   *
   * @param array $data
   *   An array of default.
   */
  protected function prepopulateData(array &$data) {
    // Get prepopulate data.
    if ($this->getWebformSetting('form_prepopulate')) {
      $prepopulate_data = $this->getRequest()->query->all();
    }
    else {
      $prepopulate_data = array_intersect_key(
        $this->getRequest()->query->all(),
        $this->getWebform()->getElementsPrepopulate()
      );
    }

    // Validate prepopulate data.
    foreach ($prepopulate_data as $element_key => &$value) {
      if ($this->checkPrepopulateDataValid($element_key, $value) === FALSE) {
        unset($prepopulate_data[$element_key]);
      }
    }

    // Set prepopulate data.
    $data = $prepopulate_data + $data;
  }

  /**
   * Determine if element prepopulate data is valid.
   *
   * @param string $element_key
   *   An element key.
   * @param string|array &$value
   *   A value.
   *
   * @return bool
   *   TRUE if element prepopulate data is valid.
   */
  protected function checkPrepopulateDataValid($element_key, &$value) {
    // Make sure the element exists.
    $element = $this->getWebform()->getElement($element_key);
    if (!$element) {
      return FALSE;
    }

    // Make sure the element is an input.
    $element_plugin = $this->elementManager->getElementInstance($element);
    if (!$element_plugin->isInput($element)) {
      return FALSE;
    }

    // Validate entity references.
    // @see \Drupal\Core\Entity\Element\EntityAutocomplete::validateEntityAutocomplete
    // @see \Drupal\webform\Plugin\WebformElement\WebformTermReferenceTrait
    if ($element_plugin instanceof WebformElementEntityReferenceInterface) {
      if (isset($element['#vocabulary'])) {
        $vocabulary_id = $element['#vocabulary'];
        $options = [
          'target_type' => 'taxonomy_term',
          'handler' => 'default:taxonomy_term',
          'target_bundles' => [$vocabulary_id => $vocabulary_id],
        ];
      }
      elseif (isset($element['#selection_settings'])) {
        $options = $element['#selection_settings'] + [
          'target_type' => $element['#target_type'],
          'handler' => $element['#selection_handler'],
        ];
      }
      else {
        return TRUE;
      }

      /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
      $handler = $this->selectionManager->getInstance($options);
      $valid_ids = $handler->validateReferenceableEntities((array) $value);
      if (empty($valid_ids)) {
        return FALSE;
      }
      else {
        $value = $element_plugin->hasMultipleValues($element) ? $valid_ids : reset($valid_ids);
        return TRUE;
      }
    }

    // Validate options.
    $is_options_element = isset($element['#options'])
      && $element_plugin instanceof OptionsBase
      && !$element_plugin instanceof WebformElementOtherInterface;
    if ($is_options_element) {
      $option_values = WebformOptionsHelper::validateOptionValues($element['#options'], (array) $value);
      if (empty($option_values)) {
        return FALSE;
      }
      else {
        $value = $element_plugin->hasMultipleValues($element) ? $option_values : reset($option_values);
        return TRUE;
      }
    }

    return TRUE;
  }

  /**
   * Populate webform elements.
   *
   * @param array $elements
   *   An render array representing elements.
   * @param array $values
   *   An array of values used to populate the elements.
   */
  protected function populateElements(array &$elements, array $values) {
    foreach ($elements as $key => &$element) {
      if (!WebformElementHelper::isElement($element, $key)) {
        continue;
      }

      // If value is not set, continue to populate sub-elements.
      if (!isset($values[$key])) {
        $this->populateElements($element, $values);
        continue;
      }

      // Get the element's plugin.
      $element_plugin = $this->elementManager->getElementInstance($element);

      // If not input, populate sub-elements and continue.
      if (!$element_plugin || !$element_plugin->isInput($element)) {
        $this->populateElements($element, $values);
        continue;
      }

      // If input does not support prepopulate, populate sub-elements and continue.
      if ($this->getRequest()->query->has($key) && !$element_plugin->hasProperty('prepopulate')) {
        $this->populateElements($element, $values);
        continue;
      }

      // Determine if this is a hidden element.
      // Hidden elements use #value but need to use #default_value to
      // be populated.
      $is_hidden = ($element_plugin instanceof Hidden);

      // Populate default value or value.
      if ($element_plugin->hasProperty('default_value') || $is_hidden) {
        $element['#default_value'] = $values[$key];
      }
      elseif ($element_plugin->hasProperty('value')) {
        $element['#value'] = $values[$key];
      }

      // API values need to trigger validation.
      if ($this->operation === 'api') {
        $element['#needs_validation'] = TRUE;
      }

      // Populate sub-elements.
      $this->populateElements($element, $values);
    }
  }

  /* ************************************************************************ */
  // Cache related functions.
  /* ************************************************************************ */

  /**
   * Add cache dependency to the form's render array.
   *
   * @param array &$form
   *   The form's render array to update.
   */
  protected function addCacheableDependency(array &$form) {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();

    // All anonymous submissions are tracked in the $_SESSION.
    // @see \Drupal\webform\WebformSubmissionStorage::setAnonymousSubmission
    /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
    $submission_storage = $this->entityTypeManager->getStorage('webform_submission');
    if ($this->currentUser()->isAnonymous() && $submission_storage->hasAnonymousSubmissionTracking($webform_submission)) {
      $form['#cache']['contexts'][] = 'session';
    }
    // Allow all form elements to be prepopulated via the URL.
    if ($this->getWebformSetting('form_prepopulate')) {
      $form['#cache']['contexts'][] = 'url.query_args';
    }
    else {
      // Allow specific form elements to be prepopulated via the URL.
      $elements_prepopulate = $this->getWebform()->getElementsPrepopulate();
      if ($elements_prepopulate) {
        foreach ($elements_prepopulate as $element_key) {
          $form['#cache']['contexts'][] = 'url.query_args:' . $element_key;
        }
      }
      // Allow source entity type and id to be passed via the URL.
      if ($this->getWebformSetting('form_prepopulate_source_entity')) {
        $form['#cache']['contexts'][] = 'url.query_args:source_entity_type';
        $form['#cache']['contexts'][] = 'url.query_args:source_entity_id';
      }
      // Allow variants to be passed.
      if ($this->getWebform()->hasVariants()) {
        $form['#cache']['contexts'][] = 'url.query_args:_webform_variant';
      }
    }
  }

  /* ************************************************************************ */
  // Account related functions.
  /* ************************************************************************ */

  /**
   * Check webform submission total limits.
   *
   * @return bool
   *   TRUE if webform submission total limit have been met.
   */
  protected function checkTotalLimit() {
    $webform = $this->getWebform();

    // Get limit total to unique submission per webform/source entity.
    $limit_total_unique = $this->getWebformSetting('limit_total_unique');

    // Check per source entity total limit.
    $entity_limit_total = $this->getWebformSetting('entity_limit_total');
    $entity_limit_total_interval = $this->getWebformSetting('entity_limit_total_interval');
    if ($limit_total_unique) {
      $entity_limit_total = 1;
      $entity_limit_total_interval = NULL;
    }
    if ($entity_limit_total && ($source_entity = $this->getLimitSourceEntity())) {
      if ($this->getStorage()->getTotal($webform, $source_entity, NULL, ['interval' => $entity_limit_total_interval]) >= $entity_limit_total) {
        return TRUE;
      }
    }

    // Check total limit.
    $limit_total = $this->getWebformSetting('limit_total');
    $limit_total_interval = $this->getWebformSetting('limit_total_interval');
    if ($limit_total_unique) {
      $limit_total = 1;
      $limit_total_interval = NULL;
    }
    if ($limit_total && $this->getStorage()->getTotal($webform, NULL, NULL, ['interval' => $limit_total_interval]) >= $limit_total) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Check webform submission user limit.
   *
   * @return bool
   *   TRUE if webform submission user limit have been met.
   */
  protected function checkUserLimit() {
    // Allow anonymous and authenticated users edit own submission.
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    if ($webform_submission->id() && $webform_submission->isOwner($this->currentUser())) {
      return FALSE;
    }

    // Get the submission owner and not current user.
    // This takes into account when an API submission changes the owner id.
    // @see \Drupal\webform\WebformSubmissionForm::submitFormValues
    $account = $this->entity->getOwner();
    $webform = $this->getWebform();

    // Check per source entity user limit.
    $entity_limit_user = $this->getWebformSetting('entity_limit_user');
    $entity_limit_user_interval = $this->getWebformSetting('entity_limit_user_interval');
    if ($entity_limit_user && ($source_entity = $this->getLimitSourceEntity())) {
      if ($this->getStorage()->getTotal($webform, $source_entity, $account, ['interval' => $entity_limit_user_interval]) >= $entity_limit_user) {
        return TRUE;
      }
    }

    // Check user limit.
    $limit_user = $this->getWebformSetting('limit_user');
    $limit_user_interval = $this->getWebformSetting('limit_user_interval');
    if ($limit_user && $this->getStorage()->getTotal($webform, NULL, $account, ['interval' => $limit_user_interval]) >= $limit_user) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Determine if drafts are enabled.
   *
   * @return bool
   *   TRUE if drafts are enabled.
   */
  protected function draftEnabled() {
    // Can't saved drafts when saving results is disabled.
    if ($this->getWebformSetting('results_disabled')) {
      return FALSE;
    }

    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();

    // Once a form is completed drafts are no longer applicable.
    if ($webform_submission->isCompleted()) {
      return FALSE;
    }

    switch ($this->getWebformSetting('draft')) {
      case WebformInterface::DRAFT_ALL:
        return TRUE;

      case WebformInterface::DRAFT_AUTHENTICATED:
        return $webform_submission->getOwner()->isAuthenticated();

      case WebformInterface::DRAFT_NONE:
      default:
        return FALSE;
    }
  }

  /**
   * Determine if reset is enabled.
   *
   * @return bool
   *   TRUE if reset is enabled.
   */
  protected function resetEnabled() {
    return $this->getWebformSetting('form_reset', FALSE);
  }

  /**
   * Returns the webform confidential indicator.
   *
   * @return bool
   *   TRUE if the webform is confidential.
   */
  protected function isConfidential() {
    return $this->getWebformSetting('form_confidential', FALSE);
  }

  /**
   * Is client side validation disabled (using the webform novalidate attribute).
   *
   * @return bool
   *   TRUE if the client side validation disabled.
   */
  protected function isFormNoValidate() {
    return $this->getWebformSetting('form_novalidate', FALSE);
  }

  /**
   * Is the webform being initially loaded via GET method.
   *
   * @return bool
   *   TRUE if the webform is being initially loaded via GET method.
   */
  protected function isGet() {
    return ($this->getRequest()->getMethod() === 'GET') ? TRUE : FALSE;
  }

  /**
   * Determine if the current request is a specific route (name).
   *
   * @param string $route_name
   *   A route name.
   *
   * @return bool
   *   TRUE if the current request is a specific route (name).
   */
  protected function isRoute($route_name) {
    return ($this->requestHandler->getRouteName($this->getEntity(), $this->getSourceEntity(), $route_name) === $this->getRouteMatch()->getRouteName()) ? TRUE : FALSE;
  }

  /**
   * Is the current webform an entity reference from the source entity.
   *
   * @return bool
   *   TRUE is the current webform an entity reference from the source entity.
   */
  protected function isWebformEntityReferenceFromSourceEntity() {
    if (!$this->sourceEntity) {
      return FALSE;
    }

    $webform = $this->webformEntityReferenceManager->getWebform($this->sourceEntity);
    if (!$webform) {
      return FALSE;
    }

    return ($webform->id() === $this->getWebform()->id()) ? TRUE : FALSE;
  }

  /* ************************************************************************ */
  // Helper functions.
  /* ************************************************************************ */

  /**
   * Get the webform submission's webform.
   *
   * @return \Drupal\webform\WebformInterface
   *   A webform.
   */
  public function getWebform() {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();
    return $webform_submission->getWebform();
  }

  /**
   * Get the webform submission's source entity.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   The webform submission's source entity.
   */
  protected function getSourceEntity() {
    return $this->sourceEntity;
  }

  /**
   * Get the webform submission entity storage.
   *
   * @return \Drupal\Webform\WebformSubmissionStorageInterface
   *   The webform submission entity storage.
   */
  protected function getStorage() {
    return $this->entityTypeManager->getStorage('webform_submission');
  }

  /**
   * Get the message manager.
   *
   * We need to wrap the message manager service because the webform submission
   * entity is being continuous cloned and updated during form processing.
   *
   * @see \Drupal\Core\Entity\EntityForm::buildEntity
   */
  protected function getMessageManager() {
    $this->messageManager->setWebformSubmission($this->getEntity());
    return $this->messageManager;
  }

  /**
   * Get source entity for use with entity limit total and user submissions.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The webform submission's source entity.
   */
  protected function getLimitSourceEntity() {
    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
    $webform_submission = $this->getEntity();

    $source_entity = $webform_submission->getSourceEntity();
    if ($source_entity && $source_entity->getEntityTypeId() !== 'webform') {
      return $source_entity;
    }
    return NULL;
  }

  /**
   * Get a webform submission's webform setting.
   *
   * @param string $name
   *   Setting name.
   * @param null|mixed $default_value
   *   Default value.
   *
   * @return mixed
   *   A webform setting.
   */
  protected function getWebformSetting($name, $default_value = NULL) {
    $value = $this->getWebform()->getSetting($name)
      ?: $this->config('webform.settings')->get('settings.default_' . $name)
      ?: NULL;

    if ($value !== NULL) {
      return $this->tokenManager->replace($value, $this->getEntity(), [], [], $this->bubbleableMetadata);
    }
    else {
      return $default_value;
    }
  }

  /* ************************************************************************ */
  // Share functions.
  /* ************************************************************************ */

  /**
   * Determine if the submission form is being embedded in a share page.
   *
   * @return bool
   *   TRUE the submission form is being embedded in a share page.
   */
  protected function isSharePage() {
    $route_name = $this->getRouteMatch()->getRouteName();
    return ($route_name && strpos($route_name, 'entity.webform.share_page') === 0);
  }

  /* ************************************************************************ */
  // Ajax functions.
  // @see \Drupal\webform\Form\WebformAjaxFormTrait
  /* ************************************************************************ */

  /**
   * {@inheritdoc}
   */
  protected function isAjax() {
    if ($this->operation === 'api') {
      return FALSE;
    }

    // Disable Ajax if the form has its #method set to 'get'.
    $elements = $this->getWebform()->getElementsInitialized();
    if (isset($elements['#method']) && $elements['#method'] === 'get') {
      return FALSE;
    }

    return $this->getWebformSetting('ajax', FALSE);
  }

  /**
   * {@inheritdoc}
   */
  public function cancelAjaxForm(array &$form, FormStateInterface $form_state) {
    throw new \Exception('Webform submission Ajax form should never be cancelled. Only ::reset should be called.');
  }

  /**
   * {@inheritdoc}
   */
  public function validateAjaxForm(array &$form, FormStateInterface $form_state) {
    if (!$this->isCallableAjaxCallback($form, $form_state)) {
      // Invalidate cache tags to prevent any caching issues.
      // @see https://www.drupal.org/project/drupal/issues/2352009
      Cache::invalidateTags(['webform:' . $this->getWebform()->id()]);
      $this->missingAjaxCallback($form, $form_state);
    }
  }

  /* ************************************************************************ */
  // API helper functions.
  /* ************************************************************************ */

  /**
   * Programmatically check that a webform is open to new submissions.
   *
   * @param \Drupal\webform\WebformInterface $webform
   *   A webform.
   *
   * @return array|bool
   *   Return TRUE if the webform is open to new submissions else returns
   *   an error message.
   *
   * @see \Drupal\webform\WebformSubmissionForm::getCustomForm
   */
  public static function isOpen(WebformInterface $webform) {
    $webform_submission = WebformSubmission::create(['webform_id' => $webform->id()]);

    /** @var \Drupal\webform\WebformSubmissionForm $form_object */
    $form_object = \Drupal::entityTypeManager()->getFormObject('webform_submission', 'add');
    $form_object->setEntity($webform_submission);

    /** @var \Drupal\webform\WebformMessageManagerInterface $message_manager */
    $message_manager = \Drupal::service('webform.message_manager');
    $message_manager->setWebformSubmission($webform_submission);

    // Check form is open.
    if ($webform->isClosed()) {
      if ($webform->isOpening()) {
        return $message_manager->get(WebformMessageManagerInterface::FORM_OPEN_MESSAGE);
      }
      else {
        return $message_manager->get(WebformMessageManagerInterface::FORM_CLOSE_MESSAGE);
      }
    }

    // Check total limit.
    if ($form_object->checkTotalLimit()) {
      return $message_manager->get(WebformMessageManagerInterface::LIMIT_TOTAL_MESSAGE);
    }

    // Check user limit.
    if ($form_object->checkUserLimit()) {
      return $message_manager->get(WebformMessageManagerInterface::LIMIT_USER_MESSAGE);
    }

    return TRUE;
  }

  /**
   * Programmatically validate form values and submit a webform submission.
   *
   * @param array $values
   *   An array of submission form values and data.
   *
   * @return array|null
   *   An array of error messages if validation fails
   *   or NULL if there are no validation errors.
   */
  public static function validateFormValues(array $values) {
    return static::submitFormValues($values, TRUE);
  }

  /**
   * Programmatically validate form values and submit a webform submission.
   *
   * @param array $values
   *   An array of submission form values and data.
   * @param bool $validate_only
   *   Flag to trigger only webform validation. Defaults to FALSE.
   *
   * @return array|\Drupal\webform\WebformSubmissionInterface|null
   *   An array of error messages if validation fails
   *   or a webform submission (when $validate_only is FALSE)
   *   or NULL (when $validate_only is TRUE) if there are no validation errors.
   */
  public static function submitFormValues(array $values, $validate_only = FALSE) {
    $webform_submission = WebformSubmission::create($values);
    return static::submitWebformSubmission($webform_submission, $validate_only);
  }

  /**
   * Programmatically validate and submit a webform submission.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   WebformSubmission with values and data.
   *
   * @return array|null
   *   An array of error messages if validation fails or
   *   NULL if there are no validation errors.
   */
  public static function validateWebformSubmission(WebformSubmissionInterface $webform_submission) {
    return static::submitWebformSubmission($webform_submission, TRUE);
  }

  /**
   * Programmatically validate and submit a webform submission.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   WebformSubmission with values and data.
   * @param bool $validate_only
   *   Flag to trigger only webform validation. Defaults to FALSE.
   *
   * @return array|\Drupal\webform\WebformSubmissionInterface|null
   *   An array of error messages if validation fails
   *   or a webform submission (when $validate_only is FALSE)
   *   or NULL (when $validate_only is TRUE) if there are no validation errors.
   */
  public static function submitWebformSubmission(WebformSubmissionInterface $webform_submission, $validate_only = FALSE) {
    /** @var \Drupal\webform\WebformSubmissionForm $form_object */
    $form_object = \Drupal::entityTypeManager()->getFormObject('webform_submission', 'api');
    $form_object->setEntity($webform_submission);

    // Create an empty form state which will be populated when the submission
    // form is submitted.
    $form_state = new FormState();

    // Set the triggering element to an empty element to prevent
    // errors from managed files.
    // @see \Drupal\file\Element\ManagedFile::validateManagedFile
    $form_state->setTriggeringElement(['#parents' => []]);

    // Get existing error messages.
    $error_messages = \Drupal::messenger()->messagesByType(MessengerInterface::TYPE_ERROR);

    // Submit the form.
    \Drupal::formBuilder()->submitForm($form_object, $form_state);

    // Get the errors but skip drafts.
    $errors = ($webform_submission->isDraft() && !$validate_only) ? [] : $form_state->getErrors();

    // Delete all form related error messages.
    \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);

    // Restore existing error message.
    foreach ($error_messages as $error_message) {
      \Drupal::messenger()->addError($error_message);
    }

    if ($errors) {
      return $errors;
    }
    elseif ($validate_only) {
      return NULL;
    }
    else {
      $webform_submission->save();
      return $webform_submission;
    }
  }

}
