123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404 |
- <?php
- namespace Drupal\Core\Form;
- use Drupal\Component\Utility\Crypt;
- 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\Theme\ThemeManagerInterface;
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
- use Symfony\Component\HttpFoundation\FileBag;
- 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 {
- /**
- * The module handler.
- *
- * @var \Drupal\Core\Extension\ModuleHandlerInterface
- */
- protected $moduleHandler;
- /**
- * The event dispatcher.
- *
- * @var \Symfony\Component\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;
- /**
- * @var \Drupal\Core\Form\FormValidatorInterface
- */
- protected $formValidator;
- /**
- * @var \Drupal\Core\Form\FormSubmitterInterface
- */
- protected $formSubmitter;
- /**
- * The form cache.
- *
- * @var \Drupal\Core\Form\FormCacheInterface
- */
- protected $formCache;
- /**
- * 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',
- ];
- /**
- * 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\Component\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) || !($form_arg instanceof FormInterface)) {
- throw new \InvalidArgumentException("The form argument $form_arg is not a valid form.");
- }
- // 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_id, FormStateInterface &$form_state) {
- // 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'].
- $input = $form_state->getUserInput();
- if (!isset($input)) {
- $input = $form_state->isMethodType('get') ? $request->query->all() : $request->request->all();
- $form_state->setUserInput($input);
- }
- 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.
- $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.
- $request = $this->requestStack->getCurrentRequest();
- 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->request->has('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->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. 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
- 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();
- }
- // 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);
- // \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()) {
- // In case of errors, do not break HTML IDs of other forms.
- Html::resetSeenIds();
- }
- // 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);
- }
- }
- /**
- * #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']],
- ];
- }
- /**
- * #lazy_builder callback; renders form CSRF token.
- *
- * @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 placholder uses a fixed string
- // that is Crypt::hashBase64('Drupal\Core\Form\FormBuilder::prepareForm');
- $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') {
- // 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,
- ];
- }
- // 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 requires this value, so ensure the
- // submitted form value appears literally, regardless of custom #tree
- // and #parents being set elsewhere.
- '#parents' => ['form_build_id'],
- // Prevent user agents from prefilling the build id with earlier values.
- // When the ajax command "update_build_id" is executed, the user agent
- // will assume that a user interaction changed the field. Upon a soft
- // reload of the page, the previous build id will be restored in the
- // input, causing subsequent ajax callbacks to access the wrong cached
- // form build. Setting the autocomplete attribute to "off" will tell the
- // user agent to never reuse the value.
- // @see https://www.w3.org/TR/2011/WD-html5-20110525/common-input-element-attributes.html#the-autocomplete-attribute
- '#attributes' => [
- 'autocomplete' => 'off',
- ],
- ];
- // 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.
- // Generates a 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 requires this value, so 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. This ensures 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 requires this value, so 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, its 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'];
- }
- }
- // 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 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();
- }
- // @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);
- }
- /**
- * {@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);
- // Make sure file uploads do not get processed.
- $this->requestStack->getCurrentRequest()->files = new FileBag();
- }
- }
- }
- 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, 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']);
- }
- // 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) {
- // The same static class method callable may be formatted in two array and
- // two string forms:
- // ['\Classname', 'methodname']
- // ['Classname', 'methodname']
- // '\Classname::methodname'
- // 'Classname::methodname'
- 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 in file.inc, 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']) && (($form_state->isProgrammed() && $form_state->isBypassingProgrammedAccessChecks()) || ($form_state->isProcessingInput() && (!isset($element['#access']) || $element['#access'])));
- // Set the element's #value property.
- if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
- // @todo Once all elements are converted to plugins in
- // https://www.drupal.org/node/2311393, rely on
- // $element['#value_callback'] directly.
- $value_callable = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
- if (!is_callable($value_callable)) {
- $value_callable = '\Drupal\Core\Render\Element\FormElement::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'] = isset($element['#default_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;
- }
- // When image buttons are clicked, browsers do NOT pass the form element
- // value in \Drupal::request()->Request. Instead they pass an integer
- // representing the coordinates of the click on the button image. This means
- // that image buttons MUST have unique $form['#name'] values, but the
- // details of their \Drupal::request()->request data should be ignored.
- elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') {
- return TRUE;
- }
- return FALSE;
- }
- /**
- * Wraps file_upload_max_size().
- *
- * @return string
- * A translated string representation of the size of the file size limit
- * based on the PHP upload_max_filesize and post_max_size.
- */
- protected function getFileUploadMaxSize() {
- return file_upload_max_size();
- }
- /**
- * Gets the current active user.
- *
- * @return \Drupal\Core\Session\AccountInterface
- */
- protected function currentUser() {
- if (!$this->currentUser && \Drupal::hasService('current_user')) {
- $this->currentUser = \Drupal::currentUser();
- }
- return $this->currentUser;
- }
- }
|