FormBuilder.php
Same filename in other branches
Namespace
Drupal\Core\FormFile
-
core/
lib/ Drupal/ Core/ Form/ FormBuilder.php
View source
<?php
namespace Drupal\Core\Form;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Environment;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\Exception\BrokenPostRequestException;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\FileBag;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
/**
* Provides form building and processing.
*
* @ingroup form_api
*/
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface, TrustedCallbackInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The CSRF token generator to validate the form token.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The form validator.
*
* @var \Drupal\Core\Form\FormValidatorInterface
*/
protected $formValidator;
/**
* The form submitter.
*
* @var \Drupal\Core\Form\FormSubmitterInterface
*/
protected $formSubmitter;
/**
* The form cache.
*
* @var \Drupal\Core\Form\FormCacheInterface
*/
protected $formCache;
/**
* Defines callables that are safe to run with invalid CSRF tokens.
*
* These Element value callables 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\\FormElementBase::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',
];
/**
* Constructs a new FormBuilder.
*
* @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
* The form cache.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @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.
* @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) {
$this->formValidator = $form_validator;
$this->formSubmitter = $form_submitter;
$this->formCache = $form_cache;
$this->moduleHandler = $module_handler;
$this->eventDispatcher = $event_dispatcher;
$this->requestStack = $request_stack;
$this->classResolver = $class_resolver;
$this->elementInfo = $element_info;
$this->csrfToken = $csrf_token;
$this->themeManager = $theme_manager;
}
/**
* {@inheritdoc}
*/
public function getFormId($form_arg, FormStateInterface &$form_state) {
// If the $form_arg is the name of a class, instantiate it. Don't allow
// arbitrary strings to be passed to the class resolver.
if (is_string($form_arg) && class_exists($form_arg)) {
$form_arg = $this->classResolver
->getInstanceFromDefinition($form_arg);
}
if (!is_object($form_arg)) {
throw new \InvalidArgumentException("The form class {$form_arg} could not be found or loaded.");
}
elseif (!$form_arg instanceof FormInterface) {
throw new \InvalidArgumentException('The form argument ' . $form_arg::class . ' must be an instance of \\Drupal\\Core\\Form\\FormInterface.');
}
// 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();
}
/**
* {@inheritdoc}
*/
public function getForm($form_arg) {
$form_state = new FormState();
$args = func_get_args();
// Remove $form_arg from the arguments.
unset($args[0]);
$form_state->addBuildInfo('args', array_values($args));
return $this->buildForm($form_arg, $form_state);
}
/**
* {@inheritdoc}
*/
public function buildForm($form_arg, FormStateInterface &$form_state) {
// Ensure the form ID is prepared.
$form_id = $this->getFormId($form_arg, $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'].
$input = $form_state->getUserInput();
if (!isset($input)) {
$input = $form_state->isMethodType('get') ? $request->query
->all() : $request->request
->all();
$form_state->setUserInput($input);
}
if ($request->getSession()
->has('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().
$session = $request->getSession();
$form_state = $session->get('batch_form_state');
$session->remove('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.
$check_cache = isset($input['form_id']) && $input['form_id'] == $form_id && !empty($input['form_build_id']);
if ($check_cache) {
$form = $this->getCache($input['form_build_id'], $form_state);
}
// 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) {
$form_state_before_retrieval = clone $form_state;
}
$form = $this->retrieveForm($form_id, $form_state);
$this->prepareForm($form_id, $form, $form_state);
// self::setCache() removes uncacheable $form_state keys (see properties
// in \Drupal\Core\Form\FormState) in order for multi-step forms to work
// properly. This means that form processing logic for single-step forms
// using $form_state->isCached() may depend on data stored in those keys
// 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
// 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();
$form_state = $form_state_before_retrieval;
$form_state->setFormState($cache_form_state);
}
}
// If this form is an AJAX request, disable all form redirects.
if ($ajax_form_request = $request->query
->has(static::AJAX_FORM_REQUEST)) {
$form_state->disableRedirect();
}
// 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
// corresponding elements and into $form_state->getValues().
// - 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 && !$request->get('form_id')) {
throw new BrokenPostRequestException($this->getFileUploadMaxSize());
}
// 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.
// Only do this when the form ID matches, since there is no guarantee from
// $ajax_form_request that it's an AJAX request for this particular form.
if ($ajax_form_request && $form_state->isProcessingInput() && $request->get('form_id') == $form_id) {
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. In order to
// resolve this issue properly it is necessary to completely separate form
// submission from rendering.
// @see https://www.drupal.org/node/2367555
if ($response instanceof Response) {
throw new EnforcedResponseException($response);
}
// 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
// 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}
*/
public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) {
$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();
}
// \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()) {
// We only reset HTML ID's when there are no validation errors as this can
// cause ID collisions with other forms on the page otherwise.
Html::resetSeenIds();
}
// 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.
// 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) {
$form['#build_id'] = $old_form['#build_id'];
if ($old_form_is_mutable_copy) {
$form['#build_id_old'] = $old_form['#build_id_old'];
}
}
else {
if (isset($old_form['#build_id'])) {
$form['#build_id_old'] = $old_form['#build_id'];
}
$form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
}
// #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'])) {
$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()) {
$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([]);
// Return a fully built form that is ready for rendering.
return $this->doBuildForm($form_id, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function getCache($form_build_id, FormStateInterface $form_state) {
return $this->formCache
->getCache($form_build_id, $form_state);
}
/**
* {@inheritdoc}
*/
public function setCache($form_build_id, $form, FormStateInterface $form_state) {
$this->formCache
->setCache($form_build_id, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function deleteCache($form_build_id) {
$this->formCache
->deleteCache($form_build_id);
}
/**
* {@inheritdoc}
*/
public function submitForm($form_arg, FormStateInterface &$form_state) {
$build_info = $form_state->getBuildInfo();
if (empty($build_info['args'])) {
$args = func_get_args();
// Remove $form and $form_state from the arguments.
unset($args[0], $args[1]);
$form_state->addBuildInfo('args', array_values($args));
}
// Populate FormState::$input with the submitted values before retrieving
// the form, to be consistent with what self::buildForm() does for
// non-programmatic submissions (form builder functions may expect it to be
// there).
$form_state->setUserInput($form_state->getValues());
$form_state->setProgrammed();
$form_id = $this->getFormId($form_arg, $form_state);
$form = $this->retrieveForm($form_id, $form_state);
// Programmed forms are always submitted.
$form_state->setSubmitted();
// Reset form validation.
$form_state->setValidationEnforced();
$form_state->clearErrors();
$this->prepareForm($form_id, $form, $form_state);
$this->processForm($form_id, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function retrieveForm($form_id, FormStateInterface &$form_state) {
// Record the $form_id.
$form_state->addBuildInfo('form_id', $form_id);
// 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'];
$callback = [
$form_state->getFormObject(),
'buildForm',
];
$form = [];
// 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);
// 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']);
}
// 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([
$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
if ($form instanceof Response) {
throw new EnforcedResponseException($form);
}
$form['#form_id'] = $form_id;
return $form;
}
/**
* {@inheritdoc}
*/
public function processForm($form_id, &$form, FormStateInterface &$form_state) {
$form_state->setValues([]);
// With GET, these forms are always submitted if requested.
if ($form_state->isMethodType('get') && $form_state->getAlwaysProcess()) {
$input = $form_state->getUserInput();
if (!isset($input['form_build_id'])) {
$input['form_build_id'] = $form['#build_id'];
}
if (!isset($input['form_id'])) {
$input['form_id'] = $form_id;
}
if (!isset($input['form_token']) && isset($form['#token'])) {
$input['form_token'] = $this->csrfToken
->get($form['#token']);
}
$form_state->setUserInput($input);
}
// self::doBuildForm() finishes building the form by calling element
// #process functions and mapping user input, if any, to #value properties,
// and also storing the values in $form_state->getValues(). We need to
// retain the unprocessed $form in case it needs to be cached.
$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()) {
// 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));
}
$this->formValidator
->validateForm($form_id, $form, $form_state);
// 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;
}
}
// Don't rebuild or cache form submissions invoked via self::submitForm().
if ($form_state->isProgrammed()) {
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().
// @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()) {
// Form building functions (e.g., self::handleInputElement()) may use
// $form_state->isRebuilding() to determine if they are running in the
// context of a rebuild, so ensure it is set.
$form_state->setRebuild();
$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()) {
$this->setCache($form['#build_id'], $unprocessed_form, $form_state);
}
}
/**
* Renders a form action URL. It's a #lazy_builder callback.
*
* @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',
],
],
];
}
/**
* Renders the form CSRF token. It's a #lazy_builder callback.
*
* @param string $placeholder
* A string containing a placeholder, matching the value of the form's
* #token.
*
* @return array
* A renderable array containing the CSRF token.
*/
public function renderFormTokenPlaceholder($placeholder) {
return [
'#markup' => $this->csrfToken
->get($placeholder),
'#cache' => [
'contexts' => [
'session',
],
],
];
}
/**
* {@inheritdoc}
*/
public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
$user = $this->currentUser();
$form['#type'] = 'form';
// 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.
// The placeholder uses a unique string that is returned by
// Crypt::hashBase64('Drupal\Core\Form\FormBuilder::prepareForm').
// cspell:disable-next-line
$placeholder = 'form_action_p_pvdeGsVG5zNF_XLGPTvYSKCf43t8qZYSwcfZl2uzM';
$form['#attached']['placeholders'][$placeholder] = [
'#lazy_builder' => [
'form_builder:renderPlaceholderFormAction',
[],
],
];
$form['#action'] = $placeholder;
}
// Fix the form method, if it is 'get' in $form_state, but not in $form.
if ($form_state->isMethodType('get') && !isset($form['#method'])) {
$form['#method'] = 'get';
}
// GET forms should not use a CSRF token.
if (isset($form['#method']) && $form['#method'] === 'get') {
$form += [
'#token' => FALSE,
];
}
// 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'])) {
$form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
}
$form['form_build_id'] = [
'#type' => 'hidden',
'#value' => $form['#build_id'],
'#id' => $form['#build_id'],
'#name' => 'form_build_id',
// Form processing and validation require this value. Ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => [
'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 and placeholder based on the form ID.
$placeholder = 'form_token_placeholder_' . Crypt::hashBase64($form_id);
$form['#token'] = $placeholder;
$form['form_token'] = [
'#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
'#type' => 'token',
'#default_value' => $placeholder,
// Form processing and validation require this value. Ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => [
'form_token',
],
// Instead of setting an actual CSRF token, we've set the placeholder
// in form_token's #default_value and #placeholder. These will be
// replaced at the very last moment to ensure forms with a CSRF token
// don't have poor cacheability.
'#attached' => [
'placeholders' => [
$placeholder => [
'#lazy_builder' => [
'form_builder:renderFormTokenPlaceholder',
[
$placeholder,
],
],
],
],
],
'#cache' => [
'max-age' => 0,
],
];
}
}
if (isset($form_id)) {
$form['form_id'] = [
'#type' => 'hidden',
'#value' => $form_id,
'#id' => Html::getUniqueId("edit-{$form_id}"),
// Form processing and validation require this value. Ensure the
// submitted form value appears literally, regardless of custom #tree
// and #parents being set elsewhere.
'#parents' => [
'form_id',
],
];
}
if (!isset($form['#id'])) {
$form['#id'] = Html::getUniqueId($form_id);
// Provide a selector usable by JavaScript. As the ID is unique, it's not
// possible to rely on it in JavaScript.
$form['#attributes']['data-drupal-selector'] = Html::getId($form_id);
}
$form += $this->elementInfo
->getInfo('form');
$form += [
'#tree' => FALSE,
'#parents' => [],
];
$form['#validate'][] = '::validateForm';
$form['#submit'][] = '::submitForm';
$build_info = $form_state->getBuildInfo();
// 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.
if (!isset($form['#theme'])) {
$form['#theme'] = [
$form_id,
];
if (isset($build_info['base_form_id'])) {
$form['#theme'][] = $build_info['base_form_id'];
}
}
// Add the 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form' cache tag to
// identify this render array as a form to the render cache.
$form['#cache']['tags'][] = 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form';
// Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
// hook_form_FORM_ID_alter() implementations.
$hooks = [
'form',
];
if (isset($build_info['base_form_id'])) {
$hooks[] = 'form_' . $build_info['base_form_id'];
}
$hooks[] = 'form_' . $form_id;
$this->moduleHandler
->alter($hooks, $form, $form_state, $form_id);
$this->themeManager
->alter($hooks, $form, $form_state, $form_id);
}
/**
* Builds the $form['#action'].
*
* @return string
* The URL to be used as the $form['#action'].
*/
protected function buildFormAction() {
// @todo Use <current> instead of the main request in
// https://www.drupal.org/node/2505339.
$request = $this->requestStack
->getMainRequest();
$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 (str_starts_with($request_uri, '//')) {
$request_uri = $request->getUri();
}
// @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]);
$action = $parsed['path'] . ($parsed['query'] ? '?' . UrlHelper::buildQuery($parsed['query']) : '');
return UrlHelper::filterBadProtocol($action);
}
/**
* {@inheritdoc}
*/
public function setInvalidTokenError(FormStateInterface $form_state) {
$this->formValidator
->setInvalidTokenError($form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm($form_id, &$form, FormStateInterface &$form_state) {
$this->formValidator
->validateForm($form_id, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function redirectForm(FormStateInterface $form_state) {
return $this->formSubmitter
->redirectForm($form_state);
}
/**
* {@inheritdoc}
*/
public function executeValidateHandlers(&$form, FormStateInterface &$form_state) {
$this->formValidator
->executeValidateHandlers($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function executeSubmitHandlers(&$form, FormStateInterface &$form_state) {
$this->formSubmitter
->executeSubmitHandlers($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function doSubmitForm(&$form, FormStateInterface &$form_state) {
throw new \LogicException('Use FormBuilderInterface::processForm() instead.');
}
/**
* {@inheritdoc}
*/
public function doBuildForm($form_id, &$element, FormStateInterface &$form_state) {
// Initialize as unprocessed.
$element['#processed'] = FALSE;
// Use element defaults.
if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = $this->elementInfo
->getInfo($element['#type']))) {
// Overlay $info onto $element, retaining preexisting keys in $element.
$element += $info;
$element['#defaults_loaded'] = TRUE;
}
// Assign basic defaults common for all form elements.
$element += [
'#required' => FALSE,
'#attributes' => [],
'#title_display' => 'before',
'#description_display' => 'after',
'#errors' => NULL,
];
// Special handling if we're on the top level form element.
if (isset($element['#type']) && $element['#type'] == 'form') {
if (!empty($element['#https']) && !UrlHelper::isExternal($element['#action'])) {
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.
$form_state->setCompleteForm($element);
// 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.
$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);
// Ignore all submitted values.
$form_state->setUserInput([]);
$request = $this->requestStack
->getCurrentRequest();
// Do not trust any POST data.
$request->request = new InputBag();
// Make sure file uploads do not get processed.
$request->files = new FileBag();
// Ensure PHP globals reflect these changes.
$request->overrideGlobals();
}
}
}
else {
$form_state->setProcessInput(FALSE);
}
// All form elements should have an #array_parents property.
$element['#array_parents'] = [];
}
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, it's 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, it's not
// possible to rely on it in JavaScript.
$element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']);
}
// 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), [
&$element,
&$form_state,
&$complete_form,
]);
}
$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) {
// 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']))) {
$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;
}
// Make child elements inherit their parent's #disabled and #allow_focus
// values unless they specify their own.
foreach ([
'#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'], [
$key,
]) : [
$key,
];
}
// Ensure #array_parents follows the actual form structure.
$array_parents = $element['#array_parents'];
$array_parents[] = $key;
$element[$key]['#array_parents'] = $array_parents;
// Assign a decimal placeholder weight to preserve original array order.
if (!isset($element[$key]['#weight'])) {
$element[$key]['#weight'] = $count / 1000;
}
else {
// If one of the child elements has a weight then we will need to sort
// later.
unset($element['#sorted']);
}
$element[$key] = $this->doBuildForm($form_id, $element[$key], $form_state);
$count++;
}
// The #after_build flag allows any piece of a form to be altered
// after normal input parsing has been completed.
if (isset($element['#after_build']) && !isset($element['#after_build_done'])) {
foreach ($element['#after_build'] as $callback) {
$element = call_user_func_array($form_state->prepareCallback($callback), [
$element,
&$form_state,
]);
}
$element['#after_build_done'] = TRUE;
}
// If there is a file element, we need to flip a flag so later the
// form encoding can be set.
if (isset($element['#type']) && $element['#type'] == 'file') {
$form_state->setHasFileElement();
}
// Final tasks for the form element after self::doBuildForm() has run for
// all other elements.
if (isset($element['#type']) && $element['#type'] == 'form') {
// If there is a file element, we set the form encoding.
if ($form_state->hasFileElement()) {
$element['#attributes']['enctype'] = 'multipart/form-data';
}
// Allow Ajax submissions to the form action to bypass verification. This
// is especially useful for multipart forms, which cannot be verified via
// a response header.
$element['#attached']['drupalSettings']['ajaxTrustedUrl'][$element['#action']] = TRUE;
// If a form contains a single textfield, and the ENTER key is pressed
// within it, Internet Explorer submits the form with no POST data
// identifying any submit button. Other browsers submit POST data as
// though the user clicked the first button. Therefore, to be as
// consistent as we can be across browsers, if no 'triggering_element' has
// been identified yet, default it to the first button.
$buttons = $form_state->getButtons();
if (!$form_state->isProgrammed() && !$form_state->getTriggeringElement() && !empty($buttons)) {
$form_state->setTriggeringElement($buttons[0]);
}
$triggering_element = $form_state->getTriggeringElement();
// If the triggering element specifies "button-level" validation and
// submit handlers to run instead of the default form-level ones, then add
// those to the form state.
if (isset($triggering_element['#validate'])) {
$form_state->setValidateHandlers($triggering_element['#validate']);
}
if (isset($triggering_element['#submit'])) {
$form_state->setSubmitHandlers($triggering_element['#submit']);
}
// If the triggering element executes submit handlers, then set the form
// state key that's needed for those handlers to run.
if (!empty($triggering_element['#executes_submit_callback'])) {
$form_state->setSubmitted();
}
// Special processing if the triggering element is a button.
if (!empty($triggering_element['#is_button'])) {
// Because there are several ways in which the triggering element could
// have been determined (including from input variables set by
// JavaScript or fallback behavior implemented for IE), and because
// buttons often have their #name property not derived from their
// #parents property, we can't assume that input processing that's
// happened up until here has resulted in
// $form_state->getValue(BUTTON_NAME) being set. But it's common for
// forms to have several buttons named 'op' and switch on
// $form_state->getValue('op') during submit handler execution.
$form_state->setValue($triggering_element['#name'], $triggering_element['#value']);
}
}
return $element;
}
/**
* Helper function to normalize the different callable formats.
*
* @param callable $value_callable
* The callable to be checked.
*
* @return bool
* TRUE if the callable is safe even if the CSRF token is invalid, FALSE
* otherwise.
*/
protected function valueCallableIsSafe(callable $value_callable) {
if (is_callable($value_callable, FALSE, $callable_name)) {
// The third parameter of is_callable() is set to a string form, but we
// still have to normalize further by stripping a leading '\'.
return in_array(ltrim($callable_name, '\\'), $this->safeCoreValueCallables);
}
return FALSE;
}
/**
* Adds the #name and #value properties of an input element before rendering.
*/
protected function handleInputElement($form_id, &$element, FormStateInterface &$form_state) {
if (!isset($element['#name'])) {
$name = array_shift($element['#parents']);
$element['#name'] = $name;
if ($element['#type'] == 'file') {
// To make it easier to handle files, we place all
// file fields in the 'files' array. Also, we do not support
// nested file names.
// @todo Remove this files prefix now?
$element['#name'] = 'files[' . $element['#name'] . ']';
}
elseif (count($element['#parents'])) {
$element['#name'] .= '[' . implode('][', $element['#parents']) . ']';
}
array_unshift($element['#parents'], $name);
}
// Setting #disabled to TRUE results in user input being ignored regardless
// of how the element is themed or whether JavaScript is used to change the
// control's attributes. However, it's good UI to let the user know that
// input is not wanted for the control. HTML supports two attributes for:
// this: http://www.w3.org/TR/html401/interact/forms.html#h-17.12. If a form
// wants to start a control off with one of these attributes for UI
// purposes, only, but still allow input to be processed if it's submitted,
// it can set the desired attribute in #attributes directly rather than
// using #disabled. However, developers should think carefully about the
// accessibility implications of doing so: if the form expects input to be
// enterable under some condition triggered by JavaScript, how would someone
// who has JavaScript disabled trigger that condition? Instead, developers
// should consider whether a multi-step form would be more appropriate
// (#disabled can be changed from step to step). If one still decides to use
// JavaScript to affect when a control is enabled, then it is best for
// accessibility for the control to be enabled in the HTML, and disabled by
// JavaScript on document ready.
if (!empty($element['#disabled'])) {
if (!empty($element['#allow_focus'])) {
$element['#attributes']['readonly'] = 'readonly';
}
else {
$element['#attributes']['disabled'] = 'disabled';
}
}
// With JavaScript or other easy hacking, input can be submitted even for
// elements with #access=FALSE or #disabled=TRUE. For security, these must
// not be processed. Forms that set #disabled=TRUE on an element do not
// expect input for the element, and even forms submitted with
// self::submitForm() must not be able to get around this. Forms that set
// #access=FALSE on an element usually allow access for some users, so forms
// submitted with self::submitForm() may bypass access restriction and be
// treated as high-privilege users instead.
$process_input = empty($element['#disabled']) && !in_array($element['#type'], [
'item',
'value',
], TRUE) && ($form_state->isProgrammed() && $form_state->isBypassingProgrammedAccessChecks() || $form_state->isProcessingInput() && (!isset($element['#access']) || ($element['#access'] instanceof AccessResultInterface && $element['#access']->isAllowed() || $element['#access'] === TRUE)));
// Set the element's #value property.
if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
$value_callable = $element['#value_callback'] ?? NULL;
if (!is_callable($value_callable)) {
$value_callable = '\\Drupal\\Core\\Render\\Element\\FormElementBase::valueCallback';
}
if ($process_input) {
// Get the input for the current element. NULL values in the input need
// to be explicitly distinguished from missing input. (see below)
$input_exists = NULL;
$input = NestedArray::getValue($form_state->getUserInput(), $element['#parents'], $input_exists);
// For browser-submitted forms, the submitted values do not contain
// values for certain elements (empty multiple select, unchecked
// checkbox). During initial form processing, we add explicit NULL
// values for such elements in FormState::$input. When rebuilding the
// form, we can distinguish elements having NULL input from elements
// that were not part of the initially submitted form and can therefore
// use default values for the latter, if required. Programmatically
// submitted forms can submit explicit NULL values when calling
// self::submitForm() so we do not modify FormState::$input for them.
if (!$input_exists && !$form_state->isRebuilding() && !$form_state->isProgrammed()) {
// Add the necessary parent keys to FormState::$input and sets the
// element's input value to NULL.
NestedArray::setValue($form_state->getUserInput(), $element['#parents'], NULL);
$input_exists = TRUE;
}
// If we have input for the current element, assign it to the #value
// property, optionally filtered through $value_callback.
if ($input_exists) {
// Skip all value callbacks except safe ones like text if the CSRF
// token was invalid.
if (!$form_state->hasInvalidToken() || $this->valueCallableIsSafe($value_callable)) {
$element['#value'] = call_user_func_array($value_callable, [
&$element,
$input,
&$form_state,
]);
}
else {
$input = NULL;
}
if (!isset($element['#value']) && isset($input)) {
$element['#value'] = $input;
}
}
// Mark all posted values for validation.
if (isset($element['#value']) || !empty($element['#required'])) {
$element['#needs_validation'] = TRUE;
}
}
// Load defaults.
if (!isset($element['#value'])) {
// Call #type_value without a second argument to request default_value
// handling.
$element['#value'] = call_user_func_array($value_callable, [
&$element,
FALSE,
&$form_state,
]);
// Final catch. If we haven't set a value yet, use the explicit default
// value. Avoid image buttons (which come with garbage value), so we
// only get value for the button actually clicked.
if (!isset($element['#value']) && empty($element['#has_garbage_value'])) {
$element['#value'] = $element['#default_value'] ?? '';
}
}
}
// Determine which element (if any) triggered the submission of the form and
// keep track of all the clickable buttons in the form for
// \Drupal\Core\Form\FormState::cleanValues(). Enforce the same input
// processing restrictions as above.
if ($process_input) {
// Detect if the element triggered the submission via Ajax.
if ($this->elementTriggeredScriptedSubmission($element, $form_state)) {
$form_state->setTriggeringElement($element);
}
// If the form was submitted by the browser rather than via Ajax, then it
// can only have been triggered by a button, and we need to determine
// which button within the constraints of how browsers provide this
// information.
if (!empty($element['#is_button'])) {
// All buttons in the form need to be tracked for
// \Drupal\Core\Form\FormState::cleanValues() and for the
// self::doBuildForm() code that handles a form submission containing no
// button information in \Drupal::request()->request.
$buttons = $form_state->getButtons();
$buttons[] = $element;
$form_state->setButtons($buttons);
if ($this->buttonWasClicked($element, $form_state)) {
$form_state->setTriggeringElement($element);
}
}
}
// Set the element's value in $form_state->getValues(), but only, if its key
// does not exist yet (a #value_callback may have already populated it).
if (!NestedArray::keyExists($form_state->getValues(), $element['#parents'])) {
$form_state->setValueForElement($element, $element['#value']);
}
}
/**
* Detects if an element triggered the form submission via Ajax.
*
* This detects button or non-button controls that trigger a form submission
* via Ajax or some other scriptable environment. These environments can set
* the special input key '_triggering_element_name' to identify the triggering
* element. If the name alone doesn't identify the element uniquely, the input
* key '_triggering_element_value' may also be set to require a match on
* element value. An example where this is needed is if there are several
* // buttons all named 'op', and only differing in their value.
*/
protected function elementTriggeredScriptedSubmission($element, FormStateInterface &$form_state) {
$input = $form_state->getUserInput();
if (!empty($input['_triggering_element_name']) && $element['#name'] == $input['_triggering_element_name']) {
if (empty($input['_triggering_element_value']) || $input['_triggering_element_value'] == $element['#value']) {
return TRUE;
}
}
return FALSE;
}
/**
* Determines if a given button triggered the form submission.
*
* This detects button controls that trigger a form submission by being
* clicked and having the click processed by the browser rather than being
* captured by JavaScript. Essentially, it detects if the button's name and
* value are part of the POST data, but with extra code to deal with the
* convoluted way in which browsers submit data for image button clicks.
*
* This does not detect button clicks processed by Ajax (that is done in
* self::elementTriggeredScriptedSubmission()) and it does not detect form
* submissions from Internet Explorer in response to an ENTER key pressed in a
* textfield (self::doBuildForm() has extra code for that).
*
* Because this function contains only part of the logic needed to determine
* $form_state->getTriggeringElement(), it should not be called from anywhere
* other than within the Form API. Form validation and submit handlers needing
* to know which button was clicked should get that information from
* $form_state->getTriggeringElement().
*/
protected function buttonWasClicked($element, FormStateInterface &$form_state) {
// First detect normal 'vanilla' button clicks. Traditionally, all standard
// buttons on a form share the same name (usually 'op'), and the specific
// return value is used to determine which was clicked. This ONLY works as
// long as $form['#name'] puts the value at the top level of the tree of
// \Drupal::request()->request data.
$input = $form_state->getUserInput();
// The input value attribute is treated as CDATA by browsers. This means
// that they replace character entities with characters. Therefore, we need
// to decode the value in $element['#value']. For more details see
// http://www.w3.org/TR/html401/types.html#type-cdata.
if (isset($input[$element['#name']]) && $input[$element['#name']] == Html::decodeEntities($element['#value'])) {
return TRUE;
}
elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') {
return TRUE;
}
return FALSE;
}
/**
* Wraps file_upload_max_size().
*
* @return int
* The file size limit in bytes based on the PHP upload_max_filesize and
* post_max_size.
*/
protected function getFileUploadMaxSize() : int {
return Environment::getUploadMaxSize();
}
/**
* Gets the current active user.
*
* @return \Drupal\Core\Session\AccountInterface
* The current account.
*/
protected function currentUser() {
if (!$this->currentUser && \Drupal::hasService('current_user')) {
$this->currentUser = \Drupal::currentUser();
}
return $this->currentUser;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return [
'renderPlaceholderFormAction',
'renderFormTokenPlaceholder',
];
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
FormBuilder | Provides form building and processing. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.