Newer
Older
Alex Pott
committed
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormBuilder.
*/
namespace Drupal\Core\Form;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
Alex Pott
committed
use Drupal\Component\Utility\NestedArray;
Dries Buytaert
committed
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResultInterface;
Alex Pott
committed
use Drupal\Core\Access\CsrfTokenGenerator;
Alex Pott
committed
use Drupal\Core\DependencyInjection\ClassResolverInterface;
Alex Bronstein
committed
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
Alex Pott
committed
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\Exception\BrokenPostRequestException;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
Alex Pott
committed
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\FileBag;
use Symfony\Component\HttpFoundation\RequestStack;
Alex Pott
committed
use Symfony\Component\HttpFoundation\Response;
/**
* Provides form building and processing.
*
* @ingroup form_api
Alex Pott
committed
*/
Angie Byron
committed
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface {
Alex Pott
committed
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The request stack.
Alex Pott
committed
*
* @var \Symfony\Component\HttpFoundation\RequestStack
Alex Pott
committed
*/
protected $requestStack;
Alex Pott
committed
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
Alex Pott
committed
/**
* The CSRF token generator to validate the form token.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
Alex Pott
committed
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
Alex Pott
committed
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
Alex Pott
committed
/**
Alex Pott
committed
* @var \Drupal\Core\Form\FormValidatorInterface
Angie Byron
committed
*/
Alex Pott
committed
protected $formValidator;
Angie Byron
committed
/**
* @var \Drupal\Core\Form\FormSubmitterInterface
*/
protected $formSubmitter;
Angie Byron
committed
/**
* The form cache.
*
* @var \Drupal\Core\Form\FormCacheInterface
*/
protected $formCache;
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* Defines element value callables which are safe to run even when the form
* state has an invalid CSRF token.
*
* Excluded from this list on purpose:
* - Drupal\file\Element\ManagedFile::valueCallback
* - Drupal\Core\Datetime\Element\Datelist::valueCallback
* - Drupal\Core\Datetime\Element\Datetime::valueCallback
* - Drupal\Core\Render\Element\ImageButton::valueCallback
* - Drupal\file\Plugin\Field\FieldWidget\FileWidget::value
* - color_palette_color_value
*
* @var array
*/
protected $safeCoreValueCallables = [
'Drupal\Core\Render\Element\Checkbox::valueCallback',
'Drupal\Core\Render\Element\Checkboxes::valueCallback',
'Drupal\Core\Render\Element\Email::valueCallback',
'Drupal\Core\Render\Element\FormElement::valueCallback',
'Drupal\Core\Render\Element\MachineName::valueCallback',
'Drupal\Core\Render\Element\Number::valueCallback',
'Drupal\Core\Render\Element\PathElement::valueCallback',
'Drupal\Core\Render\Element\Password::valueCallback',
'Drupal\Core\Render\Element\PasswordConfirm::valueCallback',
'Drupal\Core\Render\Element\Radio::valueCallback',
'Drupal\Core\Render\Element\Radios::valueCallback',
'Drupal\Core\Render\Element\Range::valueCallback',
'Drupal\Core\Render\Element\Search::valueCallback',
'Drupal\Core\Render\Element\Select::valueCallback',
'Drupal\Core\Render\Element\Tableselect::valueCallback',
'Drupal\Core\Render\Element\Table::valueCallback',
'Drupal\Core\Render\Element\Tel::valueCallback',
'Drupal\Core\Render\Element\Textarea::valueCallback',
'Drupal\Core\Render\Element\Textfield::valueCallback',
'Drupal\Core\Render\Element\Token::valueCallback',
'Drupal\Core\Render\Element\Url::valueCallback',
'Drupal\Core\Render\Element\Weight::valueCallback',
];
Alex Pott
committed
/**
* Constructs a new FormBuilder.
*
Alex Pott
committed
* @param \Drupal\Core\Form\FormValidatorInterface $form_validator
* The form validator.
* @param \Drupal\Core\Form\FormSubmitterInterface $form_submitter
* The form submission processor.
* @param \Drupal\Core\Form\FormCacheInterface $form_cache
Angie Byron
committed
* The form cache.
Alex Pott
committed
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
Alex Pott
committed
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
Alex Pott
committed
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ElementInfoManagerInterface $element_info, ThemeManagerInterface $theme_manager, CsrfTokenGenerator $csrf_token = NULL) {
Alex Pott
committed
$this->formValidator = $form_validator;
$this->formSubmitter = $form_submitter;
Angie Byron
committed
$this->formCache = $form_cache;
Alex Pott
committed
$this->moduleHandler = $module_handler;
$this->eventDispatcher = $event_dispatcher;
$this->requestStack = $request_stack;
Alex Pott
committed
$this->classResolver = $class_resolver;
$this->elementInfo = $element_info;
Alex Pott
committed
$this->csrfToken = $csrf_token;
$this->themeManager = $theme_manager;
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function getFormId($form_arg, FormStateInterface &$form_state) {
Alex Pott
committed
// If the $form_arg is the name of a class, instantiate it. Don't allow
// arbitrary strings to be passed to the class resolver.
Alex Pott
committed
if (is_string($form_arg) && class_exists($form_arg)) {
Alex Pott
committed
$form_arg = $this->classResolver->getInstanceFromDefinition($form_arg);
Alex Pott
committed
}
Alex Pott
committed
if (!is_object($form_arg) || !($form_arg instanceof FormInterface)) {
catch
committed
throw new \InvalidArgumentException("The form argument $form_arg is not a valid form.");
Alex Pott
committed
}
// Add the $form_arg as the callback object and determine the form ID.
$form_state->setFormObject($form_arg);
if ($form_arg instanceof BaseFormIdInterface) {
$form_state->addBuildInfo('base_form_id', $form_arg->getBaseFormId());
}
return $form_arg->getFormId();
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
public function getForm($form_arg) {
Dries Buytaert
committed
$form_state = new FormState();
Alex Pott
committed
$args = func_get_args();
// Remove $form_arg from the arguments.
unset($args[0]);
Dries Buytaert
committed
$form_state->addBuildInfo('args', array_values($args));
Alex Pott
committed
return $this->buildForm($form_arg, $form_state);
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function buildForm($form_id, FormStateInterface &$form_state) {
Angie Byron
committed
// Ensure the form ID is prepared.
$form_id = $this->getFormId($form_id, $form_state);
$request = $this->requestStack->getCurrentRequest();
// Inform $form_state about the request method that's building it, so that
// it can prevent persisting state changes during HTTP methods for which
// that is disallowed by HTTP: GET and HEAD.
$form_state->setRequestMethod($request->getMethod());
// Initialize the form's user input. The user input should include only the
// input meant to be treated as part of what is submitted to the form, so
// we base it on the form's method rather than the request's method. For
// example, when someone does a GET request for
// /node/add/article?destination=foo, which is a form that expects its
// submission method to be POST, the user input during the GET request
// should be initialized to empty rather than to ['destination' => 'foo'].
Angie Byron
committed
$input = $form_state->getUserInput();
if (!isset($input)) {
$input = $form_state->isMethodType('get') ? $request->query->all() : $request->request->all();
Angie Byron
committed
$form_state->setUserInput($input);
Alex Pott
committed
}
if (isset($_SESSION['batch_form_state'])) {
// We've been redirected here after a batch processing. The form has
// already been processed, but needs to be rebuilt. See _batch_finished().
$form_state = $_SESSION['batch_form_state'];
unset($_SESSION['batch_form_state']);
return $this->rebuildForm($form_id, $form_state);
}
// If the incoming input contains a form_build_id, we'll check the cache for
// a copy of the form in question. If it's there, we don't have to rebuild
// the form to proceed. In addition, if there is stored form_state data from
// a previous step, we'll retrieve it so it can be passed on to the form
// processing code.
Angie Byron
committed
$check_cache = isset($input['form_id']) && $input['form_id'] == $form_id && !empty($input['form_build_id']);
Alex Pott
committed
if ($check_cache) {
Angie Byron
committed
$form = $this->getCache($input['form_build_id'], $form_state);
Alex Pott
committed
}
// If the previous bit of code didn't result in a populated $form object, we
// are hitting the form for the first time and we need to build it from
// scratch.
if (!isset($form)) {
// If we attempted to serve the form from cache, uncacheable $form_state
// keys need to be removed after retrieving and preparing the form, except
// any that were already set prior to retrieving the form.
if ($check_cache) {
Dries Buytaert
committed
$form_state_before_retrieval = clone $form_state;
Alex Pott
committed
}
$form = $this->retrieveForm($form_id, $form_state);
$this->prepareForm($form_id, $form, $form_state);
Dries Buytaert
committed
// self::setCache() removes uncacheable $form_state keys (see properties
// in \Drupal\Core\Form\FormState) in order for multi-step forms to work
Alex Pott
committed
// properly. This means that form processing logic for single-step forms
// using $form_state->isCached() may depend on data stored in those keys
Dries Buytaert
committed
// during self::retrieveForm()/self::prepareForm(), but form processing
// should not depend on whether the form is cached or not, so $form_state
// is adjusted to match what it would be after a
Alex Pott
committed
// self::setCache()/self::getCache() sequence. These exceptions are
// allowed to survive here:
// - always_process: Does not make sense in conjunction with form caching
// in the first place, since passing form_build_id as a GET parameter is
// not desired.
// - temporary: Any assigned data is expected to survives within the same
// page request.
if ($check_cache) {
$cache_form_state = $form_state->getCacheableArray();
$cache_form_state['always_process'] = $form_state->getAlwaysProcess();
$cache_form_state['temporary'] = $form_state->getTemporary();
Dries Buytaert
committed
$form_state = $form_state_before_retrieval;
$form_state->setFormState($cache_form_state);
Alex Pott
committed
}
}
Alex Bronstein
committed
// If this form is an AJAX request, disable all form redirects.
$request = $this->requestStack->getCurrentRequest();
if ($ajax_form_request = $request->query->has(static::AJAX_FORM_REQUEST)) {
$form_state->disableRedirect();
}
Alex Pott
committed
// Now that we have a constructed form, process it. This is where:
// - Element #process functions get called to further refine $form.
// - User input, if any, gets incorporated in the #value property of the
Alex Pott
committed
// corresponding elements and into $form_state->getValues().
Alex Pott
committed
// - Validation and submission handlers are called.
// - If this submission is part of a multistep workflow, the form is rebuilt
// to contain the information of the next step.
// - If necessary, the form and form state are cached or re-cached, so that
// appropriate information persists to the next page request.
// All of the handlers in the pipeline receive $form_state by reference and
// can use it to know or update information about the state of the form.
$response = $this->processForm($form_id, $form, $form_state);
// In case the post request exceeds the configured allowed size
// (post_max_size), the post request is potentially broken. Add some
// protection against that and at the same time have a nice error message.
if ($ajax_form_request && !isset($form_state->getUserInput()['form_id'])) {
throw new BrokenPostRequestException($this->getFileUploadMaxSize());
}
Alex Bronstein
committed
// After processing the form, if this is an AJAX form request, interrupt
// form rendering and return by throwing an exception that contains the
// processed form and form state. This exception will be caught by
// \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException() and
// then passed through
// \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse() to
// build a proper AJAX response.
if ($ajax_form_request && $form_state->isProcessingInput()) {
throw new FormAjaxException($form, $form_state);
}
// If the form returns a response, skip subsequent page construction by
// throwing an exception.
// @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
//
// @todo Exceptions should not be used for code flow control. However, the
// Form API does not integrate with the HTTP Kernel based architecture of
// Drupal 8. In order to resolve this issue properly it is necessary to
// completely separate form submission from rendering.
// @see https://www.drupal.org/node/2367555
Alex Pott
committed
if ($response instanceof Response) {
throw new EnforcedResponseException($response);
Alex Pott
committed
}
// If this was a successful submission of a single-step form or the last
// step of a multi-step form, then self::processForm() issued a redirect to
Alex Pott
committed
// another page, or back to this page, but as a new request. Therefore, if
// we're here, it means that this is either a form being viewed initially
// before any user input, or there was a validation error requiring the form
// to be re-displayed, or we're in a multi-step workflow and need to display
// the form's next step. In any case, we have what we need in $form, and can
// return it for rendering.
return $form;
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) {
Alex Pott
committed
$form = $this->retrieveForm($form_id, $form_state);
// Only GET and POST are valid form methods. If the form receives its input
// via POST, then $form_state must be persisted when it is rebuilt between
// submissions. If the form receives its input via GET, then persisting
// state is forbidden by $form_state->setCached(), and the form must use
// the URL itself to transfer its state across steps. Although $form_state
// throws an exception based on the request method rather than the form's
// method, we base the decision to cache on the form method, because:
// - It's the form method that defines what the form needs to do to manage
// its state.
// - rebuildForm() should only be called after successful input processing,
// which means the request method matches the form method, and if not,
// there's some other error, so it's ok if an exception is thrown.
if ($form_state->isMethodType('POST')) {
$form_state->setCached();
}
Alex Pott
committed
// If only parts of the form will be returned to the browser (e.g., Ajax or
// RIA clients), or if the form already had a new build ID regenerated when
// it was retrieved from the form cache, reuse the existing #build_id.
Alex Pott
committed
// Otherwise, a new #build_id is generated, to not clobber the previous
// build's data in the form cache; also allowing the user to go back to an
// earlier build, make changes, and re-submit.
// @see self::prepareForm()
$rebuild_info = $form_state->getRebuildInfo();
$enforce_old_build_id = isset($old_form['#build_id']) && !empty($rebuild_info['copy']['#build_id']);
$old_form_is_mutable_copy = isset($old_form['#build_id_old']);
if ($enforce_old_build_id || $old_form_is_mutable_copy) {
Alex Pott
committed
$form['#build_id'] = $old_form['#build_id'];
if ($old_form_is_mutable_copy) {
$form['#build_id_old'] = $old_form['#build_id_old'];
}
Alex Pott
committed
}
else {
if (isset($old_form['#build_id'])) {
$form['#build_id_old'] = $old_form['#build_id'];
}
Angie Byron
committed
$form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
Alex Pott
committed
}
// #action defaults to $request->getRequestUri(), but in case of Ajax and
// other partial rebuilds, the form is submitted to an alternate URL, and
// the original #action needs to be retained.
if (isset($old_form['#action']) && !empty($rebuild_info['copy']['#action'])) {
Alex Pott
committed
$form['#action'] = $old_form['#action'];
}
$this->prepareForm($form_id, $form, $form_state);
// Caching is normally done in self::processForm(), but what needs to be
// cached is the $form structure before it passes through
// self::doBuildForm(), so we need to do it here.
// @todo For Drupal 8, find a way to avoid this code duplication.
if ($form_state->isCached()) {
Alex Pott
committed
$this->setCache($form['#build_id'], $form, $form_state);
}
// Clear out all group associations as these might be different when
// re-rendering the form.
$form_state->setGroups([]);
Alex Pott
committed
// Return a fully built form that is ready for rendering.
return $this->doBuildForm($form_id, $form, $form_state);
}
/**
* {@inheritdoc}
*/
Angie Byron
committed
public function getCache($form_build_id, FormStateInterface $form_state) {
return $this->formCache->getCache($form_build_id, $form_state);
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function setCache($form_build_id, $form, FormStateInterface $form_state) {
Angie Byron
committed
$this->formCache->setCache($form_build_id, $form, $form_state);
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
public function deleteCache($form_build_id) {
$this->formCache->deleteCache($form_build_id);
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function submitForm($form_arg, FormStateInterface &$form_state) {
$build_info = $form_state->getBuildInfo();
if (empty($build_info['args'])) {
Alex Pott
committed
$args = func_get_args();
Angie Byron
committed
// Remove $form and $form_state from the arguments.
unset($args[0], $args[1]);
Dries Buytaert
committed
$form_state->addBuildInfo('args', array_values($args));
Alex Pott
committed
}
Angie Byron
committed
// Populate FormState::$input with the submitted values before retrieving
Alex Pott
committed
// the form, to be consistent with what self::buildForm() does for
// non-programmatic submissions (form builder functions may expect it to be
// there).
Angie Byron
committed
$form_state->setUserInput($form_state->getValues());
Alex Pott
committed
$form_state->setProgrammed();
Alex Pott
committed
$form_id = $this->getFormId($form_arg, $form_state);
$form = $this->retrieveForm($form_id, $form_state);
// Programmed forms are always submitted.
$form_state->setSubmitted();
Alex Pott
committed
// Reset form validation.
$form_state->setValidationEnforced();
$form_state->clearErrors();
Alex Pott
committed
$this->prepareForm($form_id, $form, $form_state);
$this->processForm($form_id, $form, $form_state);
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function retrieveForm($form_id, FormStateInterface &$form_state) {
Alex Pott
committed
// Record the $form_id.
Dries Buytaert
committed
$form_state->addBuildInfo('form_id', $form_id);
Alex Pott
committed
// We save two copies of the incoming arguments: one for modules to use
// when mapping form ids to constructor functions, and another to pass to
// the constructor function itself.
$build_info = $form_state->getBuildInfo();
$args = $build_info['args'];
Alex Pott
committed
$callback = [$form_state->getFormObject(), 'buildForm'];
Alex Pott
committed
$form = array();
// Assign a default CSS class name based on $form_id.
// This happens here and not in self::prepareForm() in order to allow the
// form constructor function to override or remove the default class.
$form['#attributes']['class'][] = Html::getClass($form_id);
Alex Pott
committed
// Same for the base form ID, if any.
if (isset($build_info['base_form_id'])) {
$form['#attributes']['class'][] = Html::getClass($build_info['base_form_id']);
Alex Pott
committed
}
// We need to pass $form_state by reference in order for forms to modify it,
// since call_user_func_array() requires that referenced variables are
// passed explicitly.
$args = array_merge(array($form, &$form_state), $args);
$form = call_user_func_array($callback, $args);
// If the form returns a response, skip subsequent page construction by
// throwing an exception.
// @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
//
// @todo Exceptions should not be used for code flow control. However, the
// Form API currently allows any form builder functions to return a
// response.
// @see https://www.drupal.org/node/2363189
Alex Pott
committed
if ($form instanceof Response) {
throw new EnforcedResponseException($form);
Alex Pott
committed
}
$form['#form_id'] = $form_id;
return $form;
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function processForm($form_id, &$form, FormStateInterface &$form_state) {
$form_state->setValues([]);
Alex Pott
committed
Angie Byron
committed
// With GET, these forms are always submitted if requested.
if ($form_state->isMethodType('get') && $form_state->getAlwaysProcess()) {
Angie Byron
committed
$input = $form_state->getUserInput();
Dries Buytaert
committed
if (!isset($input['form_build_id'])) {
$input['form_build_id'] = $form['#build_id'];
Alex Pott
committed
}
Dries Buytaert
committed
if (!isset($input['form_id'])) {
$input['form_id'] = $form_id;
Alex Pott
committed
}
Dries Buytaert
committed
if (!isset($input['form_token']) && isset($form['#token'])) {
$input['form_token'] = $this->csrfToken->get($form['#token']);
Alex Pott
committed
}
Angie Byron
committed
$form_state->setUserInput($input);
Alex Pott
committed
}
// self::doBuildForm() finishes building the form by calling element
// #process functions and mapping user input, if any, to #value properties,
Alex Pott
committed
// and also storing the values in $form_state->getValues(). We need to
// retain the unprocessed $form in case it needs to be cached.
Alex Pott
committed
$unprocessed_form = $form;
$form = $this->doBuildForm($form_id, $form, $form_state);
// Only process the input if we have a correct form submission.
if ($form_state->isProcessingInput()) {
Angie Byron
committed
// Form values for programmed form submissions typically do not include a
// value for the submit button. But without a triggering element, a
// potentially existing #limit_validation_errors property on the primary
// submit button is not taken account. Therefore, check whether there is
// exactly one submit button in the form, and if so, automatically use it
// as triggering_element.
$buttons = $form_state->getButtons();
if ($form_state->isProgrammed() && !$form_state->getTriggeringElement() && count($buttons) == 1) {
$form_state->setTriggeringElement(reset($buttons));
Angie Byron
committed
}
Alex Pott
committed
$this->formValidator->validateForm($form_id, $form, $form_state);
Alex Pott
committed
// \Drupal\Component\Utility\Html::getUniqueId() maintains a cache of
// element IDs it has seen, so it can prevent duplicates. We want to be
// sure we reset that cache when a form is processed, so scenarios that
// result in the form being built behind the scenes and again for the
// browser don't increment all the element IDs needlessly.
if (!FormState::hasAnyErrors()) {
Alex Pott
committed
// In case of errors, do not break HTML IDs of other forms.
Html::resetSeenIds();
Alex Pott
committed
}
// If there are no errors and the form is not rebuilding, submit the form.
if (!$form_state->isRebuilding() && !FormState::hasAnyErrors()) {
$submit_response = $this->formSubmitter->doSubmitForm($form, $form_state);
// If this form was cached, delete it from the cache after submission.
if ($form_state->isCached()) {
$this->deleteCache($form['#build_id']);
}
// If the form submission directly returned a response, return it now.
if ($submit_response) {
return $submit_response;
Alex Pott
committed
}
}
// Don't rebuild or cache form submissions invoked via self::submitForm().
if ($form_state->isProgrammed()) {
Alex Pott
committed
return;
}
// If $form_state->isRebuilding() has been set and input has been
// processed without validation errors, we are in a multi-step workflow
// that is not yet complete. A new $form needs to be constructed based on
// the changes made to $form_state during this request. Normally, a submit
// handler sets $form_state->isRebuilding() if a fully executed form
// requires another step. However, for forms that have not been fully
// executed (e.g., Ajax submissions triggered by non-buttons), there is no
// submit handler to set $form_state->isRebuilding(). It would not make
// sense to redisplay the identical form without an error for the user to
// correct, so we also rebuild error-free non-executed forms, regardless
// of $form_state->isRebuilding().
Alex Pott
committed
// @todo Simplify this logic; considering Ajax and non-HTML front-ends,
// along with element-level #submit properties, it makes no sense to
// have divergent form execution based on whether the triggering element
// has #executes_submit_callback set to TRUE.
if (($form_state->isRebuilding() || !$form_state->isExecuted()) && !FormState::hasAnyErrors()) {
Alex Pott
committed
// Form building functions (e.g., self::handleInputElement()) may use
// $form_state->isRebuilding() to determine if they are running in the
Alex Pott
committed
// context of a rebuild, so ensure it is set.
Dries Buytaert
committed
$form_state->setRebuild();
Alex Pott
committed
$form = $this->rebuildForm($form_id, $form_state, $form);
}
}
// After processing the form, the form builder or a #process callback may
// have called $form_state->setCached() to indicate that the form and form
// state shall be cached. But the form may only be cached if
// $form_state->disableCache() is not called. Only cache $form as it was
// prior to self::doBuildForm(), because self::doBuildForm() must run for
// each request to accommodate new user input. Rebuilt forms are not cached
// here, because self::rebuildForm() already takes care of that.
if (!$form_state->isRebuilding() && $form_state->isCached()) {
Alex Pott
committed
$this->setCache($form['#build_id'], $unprocessed_form, $form_state);
}
}
/**
* #lazy_builder callback; renders a form action URL.
*
* @return array
* A renderable array representing the form action.
*/
public function renderPlaceholderFormAction() {
return [
'#type' => 'markup',
'#markup' => $this->buildFormAction(),
'#cache' => ['contexts' => ['url.path', 'url.query_args']],
];
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
Alex Pott
committed
$user = $this->currentUser();
$form['#type'] = 'form';
Alex Pott
committed
// Only update the action if it is not already set.
if (!isset($form['#action'])) {
// Instead of setting an actual action URL, we set the placeholder, which
// will be replaced at the very last moment. This ensures forms with
// dynamically generated action URLs don't have poor cacheability.
// Use the proper API to generate the placeholder, when we have one. See
// https://www.drupal.org/node/2562341.
$placeholder = 'form_action_' . hash('crc32b', __METHOD__);
$form['#attached']['placeholders'][$placeholder] = [
'#lazy_builder' => ['form_builder:renderPlaceholderFormAction', []],
];
$form['#action'] = $placeholder;
Alex Pott
committed
}
Alex Pott
committed
// Fix the form method, if it is 'get' in $form_state, but not in $form.
if ($form_state->isMethodType('get') && !isset($form['#method'])) {
Alex Pott
committed
$form['#method'] = 'get';
}
catch
committed
// GET forms should not use a CSRF token.
if (isset($form['#method']) && $form['#method'] === 'get') {
// Merges in a default, this means if you've explicitly set #token to the
// the $form_id on a GET form, which we don't recommend, it will work.
$form += [
'#token' => FALSE,
];
}
Alex Pott
committed
// Generate a new #build_id for this form, if none has been set already.
// The form_build_id is used as key to cache a particular build of the form.
// For multi-step forms, this allows the user to go back to an earlier
// build, make changes, and re-submit.
// @see self::buildForm()
// @see self::rebuildForm()
if (!isset($form['#build_id'])) {
Angie Byron
committed
$form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
Alex Pott
committed
}
$form['form_build_id'] = array(
'#type' => 'hidden',
'#value' => $form['#build_id'],
'#id' => $form['#build_id'],
'#name' => 'form_build_id',
// Form processing and validation requires this value, so ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => array('form_build_id'),
);
// Add a token, based on either #token or form_id, to any form displayed to
// authenticated users. This ensures that any submitted form was actually
// requested previously by the user and protects against cross site request
// forgeries.
// This does not apply to programmatically submitted forms. Furthermore,
// since tokens are session-bound and forms displayed to anonymous users are
// very likely cached, we cannot assign a token for them.
// During installation, there is no $user yet.
// Form constructors may explicitly set #token to FALSE when cross site
// request forgery is irrelevant to the form, such as search forms.
if ($form_state->isProgrammed() || (isset($form['#token']) && $form['#token'] === FALSE)) {
unset($form['#token']);
}
else {
$form['#cache']['contexts'][] = 'user.roles:authenticated';
if ($user && $user->isAuthenticated()) {
// Generate a public token based on the form id.
$form['#token'] = $form_id;
$form['form_token'] = array(
'#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
'#type' => 'token',
'#default_value' => $this->csrfToken->get($form['#token']),
// Form processing and validation requires this value, so ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => array('form_token'),
'#cache' => [
'max-age' => 0,
],
);
}
Alex Pott
committed
}
if (isset($form_id)) {
$form['form_id'] = array(
'#type' => 'hidden',
'#value' => $form_id,
'#id' => Html::getUniqueId("edit-$form_id"),
Alex Pott
committed
// Form processing and validation requires this value, so ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => array('form_id'),
);
}
if (!isset($form['#id'])) {
$form['#id'] = Html::getUniqueId($form_id);
// Provide a selector usable by JavaScript. As the ID is unique, its not
// possible to rely on it in JavaScript.
$form['#attributes']['data-drupal-selector'] = Html::getId($form_id);
Alex Pott
committed
}
$form += $this->elementInfo->getInfo('form');
Alex Pott
committed
$form += array('#tree' => FALSE, '#parents' => array());
$form['#validate'][] = '::validateForm';
$form['#submit'][] = '::submitForm';
Alex Pott
committed
$build_info = $form_state->getBuildInfo();
Alex Pott
committed
// If no #theme has been set, automatically apply theme suggestions.
// The form theme hook itself, which is rendered by form.html.twig,
// is in #theme_wrappers. Therefore, the #theme function only has to care
// for rendering the inner form elements, not the form itself.
Alex Pott
committed
if (!isset($form['#theme'])) {
$form['#theme'] = array($form_id);
if (isset($build_info['base_form_id'])) {
$form['#theme'][] = $build_info['base_form_id'];
Alex Pott
committed
}
}
// Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
// hook_form_FORM_ID_alter() implementations.
$hooks = array('form');
if (isset($build_info['base_form_id'])) {
$hooks[] = 'form_' . $build_info['base_form_id'];
Alex Pott
committed
}
$hooks[] = 'form_' . $form_id;
$this->moduleHandler->alter($hooks, $form, $form_state, $form_id);
$this->themeManager->alter($hooks, $form, $form_state, $form_id);
Alex Pott
committed
}
Alex Bronstein
committed
/**
* Builds the $form['#action'].
*
* @return string
* The URL to be used as the $form['#action'].
*/
protected function buildFormAction() {
// @todo Use <current> instead of the master request in
// https://www.drupal.org/node/2505339.
$request = $this->requestStack->getMasterRequest();
$request_uri = $request->getRequestUri();
// Prevent cross site requests via the Form API by using an absolute URL
// when the request uri starts with multiple slashes..
if (strpos($request_uri, '//') === 0) {
$request_uri = $request->getUri();
}
Alex Bronstein
committed
// @todo Remove this parsing once these are removed from the request in
// https://www.drupal.org/node/2504709.
$parsed = UrlHelper::parse($request_uri);
unset($parsed['query'][static::AJAX_FORM_REQUEST], $parsed['query'][MainContentViewSubscriber::WRAPPER_FORMAT]);
return $parsed['path'] . ($parsed['query'] ? ('?' . UrlHelper::buildQuery($parsed['query'])) : '');
}
/**
* {@inheritdoc}
*/
public function setInvalidTokenError(FormStateInterface $form_state) {
$this->formValidator->setInvalidTokenError($form_state);
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function validateForm($form_id, &$form, FormStateInterface &$form_state) {
Alex Pott
committed
$this->formValidator->validateForm($form_id, $form, $form_state);
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function redirectForm(FormStateInterface $form_state) {
return $this->formSubmitter->redirectForm($form_state);
Alex Pott
committed
}
/**
Alex Pott
committed
* {@inheritdoc}
Angie Byron
committed
*/
Dries Buytaert
committed
public function executeValidateHandlers(&$form, FormStateInterface &$form_state) {
Alex Pott
committed
$this->formValidator->executeValidateHandlers($form, $form_state);
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function executeSubmitHandlers(&$form, FormStateInterface &$form_state) {
$this->formSubmitter->executeSubmitHandlers($form, $form_state);
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function doSubmitForm(&$form, FormStateInterface &$form_state) {
throw new \LogicException('Use FormBuilderInterface::processForm() instead.');
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function doBuildForm($form_id, &$element, FormStateInterface &$form_state) {
Alex Pott
committed
// Initialize as unprocessed.
$element['#processed'] = FALSE;
// Use element defaults.
if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = $this->elementInfo->getInfo($element['#type']))) {
Alex Pott
committed
// Overlay $info onto $element, retaining preexisting keys in $element.
$element += $info;
$element['#defaults_loaded'] = TRUE;
}
// Assign basic defaults common for all form elements.
$element += array(
'#required' => FALSE,
'#attributes' => array(),
'#title_display' => 'before',
Alex Pott
committed
'#description_display' => 'after',
Angie Byron
committed
'#errors' => NULL,
Alex Pott
committed
);
// Special handling if we're on the top level form element.
if (isset($element['#type']) && $element['#type'] == 'form') {
Angie Byron
committed
if (!empty($element['#https']) && !UrlHelper::isExternal($element['#action'])) {
Alex Pott
committed
global $base_root;
// Not an external URL so ensure that it is secure.
$element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action'];
}
// Store a reference to the complete form in $form_state prior to building
// the form. This allows advanced #process and #after_build callbacks to
// perform changes elsewhere in the form.
Dries Buytaert
committed
$form_state->setCompleteForm($element);
Alex Pott
committed
// Set a flag if we have a correct form submission. This is always TRUE
// for programmed forms coming from self::submitForm(), or if the form_id
// coming from the POST data is set and matches the current form_id.
Angie Byron
committed
$input = $form_state->getUserInput();
if ($form_state->isProgrammed() || (!empty($input) && (isset($input['form_id']) && ($input['form_id'] == $form_id)))) {
$form_state->setProcessInput();
if (isset($element['#token'])) {
$input = $form_state->getUserInput();
if (empty($input['form_token']) || !$this->csrfToken->validate($input['form_token'], $element['#token'])) {
// Set an early form error to block certain input processing since
// that opens the door for CSRF vulnerabilities.
$this->setInvalidTokenError($form_state);
// This value is checked in self::handleInputElement().
$form_state->setInvalidToken(TRUE);
// Make sure file uploads do not get processed.
$this->requestStack->getCurrentRequest()->files = new FileBag();
}
}
Alex Pott
committed
}
else {
$form_state->setProcessInput(FALSE);
Alex Pott
committed
}
// All form elements should have an #array_parents property.
$element['#array_parents'] = array();
}
if (!isset($element['#id'])) {
$unprocessed_id = 'edit-' . implode('-', $element['#parents']);
$element['#id'] = Html::getUniqueId($unprocessed_id);
// Provide a selector usable by JavaScript. As the ID is unique, its not
// possible to rely on it in JavaScript.
$element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id);
}
else {
// Provide a selector usable by JavaScript. As the ID is unique, its not
// possible to rely on it in JavaScript.
$element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']);
Alex Pott
committed
}
// Add the aria-describedby attribute to associate the form control with its
// description.
if (!empty($element['#description'])) {
$element['#attributes']['aria-describedby'] = $element['#id'] . '--description';
}
// Handle input elements.
if (!empty($element['#input'])) {
$this->handleInputElement($form_id, $element, $form_state);
}
// Allow for elements to expand to multiple elements, e.g., radios,
// checkboxes and files.
if (isset($element['#process']) && !$element['#processed']) {
foreach ($element['#process'] as $callback) {
$complete_form = &$form_state->getCompleteForm();
$element = call_user_func_array($form_state->prepareCallback($callback), array(&$element, &$form_state, &$complete_form));
Alex Pott
committed
}
$element['#processed'] = TRUE;
}
// We start off assuming all form elements are in the correct order.
$element['#sorted'] = TRUE;
// Recurse through all child elements.
$count = 0;
if (isset($element['#access'])) {
$access = $element['#access'];
$inherited_access = NULL;
if (($access instanceof AccessResultInterface && !$access->isAllowed()) || $access === FALSE) {
$inherited_access = $access;
}
}
foreach (Element::children($element) as $key) {
Alex Pott
committed
// Prior to checking properties of child elements, their default
// properties need to be loaded.
if (isset($element[$key]['#type']) && empty($element[$key]['#defaults_loaded']) && ($info = $this->elementInfo->getInfo($element[$key]['#type']))) {
Alex Pott
committed
$element[$key] += $info;
$element[$key]['#defaults_loaded'] = TRUE;
}
// Don't squash an existing tree value.
if (!isset($element[$key]['#tree'])) {
$element[$key]['#tree'] = $element['#tree'];
}
// Children inherit #access from parent.
if (isset($inherited_access)) {
$element[$key]['#access'] = $inherited_access;
Alex Pott
committed
}
// Make child elements inherit their parent's #disabled and #allow_focus
// values unless they specify their own.
foreach (array('#disabled', '#allow_focus') as $property) {
if (isset($element[$property]) && !isset($element[$key][$property])) {
$element[$key][$property] = $element[$property];
}
}
// Don't squash existing parents value.
if (!isset($element[$key]['#parents'])) {
// Check to see if a tree of child elements is present. If so,
// continue down the tree if required.
$element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key);
}
// Ensure #array_parents follows the actual form structure.
$array_parents = $element['#array_parents'];
$array_parents[] = $key;