/** * {@inheritdoc} */ public function validateForm($form_id, &$form, &$form_state) { // If this form is flagged to always validate, ensure that previous runs of // validation are ignored. if (!empty($form_state['must_validate'])) { $form_state['validation_complete'] = FALSE; } // If this form has completed validation, do not validate again. if (!empty($form_state['validation_complete'])) { return; } // If the session token was set by self::prepareForm(), ensure that it // matches the current user's session. if (isset($form['#token'])) { if (!$this->csrfToken->validate($form_state['values']['form_token'], $form['#token'])) { $url = $this->requestStack->getCurrentRequest()->getRequestUri(); // Setting this error will cause the form to fail validation. $this->setErrorByName('form_token', $form_state, $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url))); // Stop here and don't run any further validation handlers, because they // could invoke non-safe operations which opens the door for CSRF // vulnerabilities. $this->finalizeValidation($form, $form_state, $form_id); return; } } // Recursively validate each form element. $this->doValidateForm($form, $form_state, $form_id); $this->finalizeValidation($form, $form_state, $form_id); $this->handleErrorsWithLimitedValidation($form, $form_state, $form_id); }
/** * Transliterates a string in given language. Various postprocessing possible. * * @param \Symfony\Component\HttpFoundation\Request $request * The input string and language for the transliteration. * Optionally may contain the replace_pattern, replace, lowercase parameters. * * @return \Symfony\Component\HttpFoundation\JsonResponse * The transliterated string. */ public function transliterate(Request $request) { $text = $request->query->get('text'); $langcode = $request->query->get('langcode'); $replace_pattern = $request->query->get('replace_pattern'); $replace_token = $request->query->get('replace_token'); $replace = $request->query->get('replace'); $lowercase = $request->query->get('lowercase'); $transliterated = $this->transliteration->transliterate($text, $langcode, '_'); if ($lowercase) { $transliterated = Unicode::strtolower($transliterated); } if (isset($replace_pattern) && isset($replace)) { if (!isset($replace_token)) { throw new AccessDeniedException("Missing 'replace_token' query parameter."); } elseif (!$this->tokenGenerator->validate($replace_token, $replace_pattern)) { throw new AccessDeniedException("Invalid 'replace_token' query parameter."); } // Quote the pattern delimiter and remove null characters to avoid the e // or other modifiers being injected. $transliterated = preg_replace('@' . strtr($replace_pattern, ['@' => '\@', chr(0) => '']) . '@', $replace, $transliterated); } return new JsonResponse($transliterated); }
/** * Tests the processOutbound() method with two parameter replacements. */ public function testProcessOutboundDynamicTwo() { $this->csrfToken->expects($this->once())->method('get')->with('100/test-path/test')->will($this->returnValue('test_token')); $route = new Route('{slug_1}/test-path/{slug_2}', array(), array('_csrf_token' => 'TRUE')); $parameters = array('slug_1' => 100, 'slug_2' => 'test'); $this->assertNull($this->processor->processOutbound('test', $route, $parameters)); }
/** * Tests the access() method with an invalid token. */ public function testAccessTokenFail() { $this->csrfToken->expects($this->once())->method('validate')->with('test_query', 'test-path')->will($this->returnValue(FALSE)); $this->routeMatch->expects($this->once())->method('getRawParameters')->will($this->returnValue(array())); $route = new Route('/test-path', array(), array('_csrf_token' => 'TRUE')); $request = Request::create('/test-path?token=test_query'); $this->assertEquals(AccessResult::forbidden()->setCacheable(FALSE), $this->accessCheck->access($route, $request, $this->routeMatch)); }
/** * {@inheritdoc} */ public function access(Route $route, Request $request, AccountInterface $account) { // Suppress notices when on other pages when menu system still checks access. $name = $request->attributes->get('name'); $token = $request->query->get('token'); $destination = $request->query->get('destination'); return $account->hasPermission('switch users') && $this->csrfToken->validate($token, "devel/switch/{$name}|" . $destination, TRUE) ? static::ALLOW : static::DENY; }
/** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { // We cannot rely on automatic token creation, since the csrf seed changes // after the redirect and the generated token is not more valid. // TODO find another way to do this. $url = Url::fromRoute('devel.switch', ['name' => $form_state->getValue('username')]); $url->setOption('query', ['token' => $this->csrfToken->get($url->getInternalPath())]); $form_state->setRedirectUrl($url); }
protected function setUp() { parent::setUp(); // Create the machine name controller. $this->tokenGenerator = $this->prophesize(CsrfTokenGenerator::class); $this->tokenGenerator->validate(Argument::cetera())->will(function ($args) { return $args[0] === 'token-' . $args[1]; }); $this->machineNameController = new MachineNameController(new PhpTransliteration(), $this->tokenGenerator->reveal()); }
/** * Tests the processOutbound() method with two parameter replacements. */ public function testProcessOutboundDynamicTwo() { $this->csrfToken->expects($this->once())->method('get')->with('100/test-path/test')->will($this->returnValue('test_token')); $route = new Route('{slug_1}/test-path/{slug_2}', array(), array('_csrf_token' => 'TRUE')); $parameters = array('slug_1' => 100, 'slug_2' => 'test'); $cacheable_metadata = new CacheableMetadata(); $this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata); // Cacheability of routes with a _csrf_token route requirement is max-age=0. $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(0), $cacheable_metadata); }
/** * Downloads a tarball of the site configuration. * * @param string $uri * The URI to download. * @param \Symfony\Component\HttpFoundation\Request $request * The request. * * @return \Symfony\Component\HttpFoundation\BinaryFileResponse * The downloaded file. */ public function downloadExport($uri, Request $request) { if ($uri) { // @todo Simplify once https://www.drupal.org/node/2630920 is solved. if (!$this->csrfToken->validate($request->query->get('token'), $uri)) { throw new AccessDeniedHttpException(); } $request = new Request(array('file' => $uri)); return $this->fileDownloadController->download($request, 'temporary'); } }
/** * {@inheritdoc} */ public function processOutbound(Route $route, array &$parameters) { if ($route->hasRequirement('_csrf_token')) { $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { $path = str_replace("{{$param}}", $value, $path); } // Adding this to the parameters means it will get merged into the query // string when the route is compiled. $parameters['token'] = $this->csrfToken->get($path); } }
/** * Tests the processOutbound() method with no _csrf_token route requirement. */ public function testProcessOutboundNoRequirement() { $this->csrfToken->expects($this->never())->method('get'); $route = new Route('/test-path'); $parameters = array(); $bubbleable_metadata = new BubbleableMetadata(); $this->processor->processOutbound('test', $route, $parameters, $bubbleable_metadata); // No parameters should be added to the parameters array. $this->assertEmpty($parameters); // Cacheability of routes without a _csrf_token route requirement is // unaffected. $this->assertEquals(new BubbleableMetadata(), $bubbleable_metadata); }
/** * {@inheritdoc} */ public function determineActiveTheme(RouteMatchInterface $route_match) { $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state'); $theme = $ajax_page_state['theme']; $token = $ajax_page_state['theme_token']; // Prevent a request forgery from giving a person access to a theme they // shouldn't be otherwise allowed to see. However, since everyone is // allowed to see the default theme, token validation isn't required for // that, and bypassing it allows most use-cases to work even when accessed // from the page cache. if ($theme === $this->configFactory->get('system.theme')->get('default') || $this->csrfGenerator->validate($token, $theme)) { return $theme; } }
/** * Index. * * @return array * Render array with all the entries. */ public function index() { $output = ['#cache' => ['max-age' => 0]]; // This is going to be reused at two places: once for the TableSortExtender, // and once for the table header itself. $header = [['data' => $this->t('Created'), 'field' => 'p.created'], ['data' => $this->t('Changed'), 'field' => 'p.changed'], ['data' => $this->t('Name'), 'field' => 'p.name'], ['data' => $this->t('Phone'), 'field' => 'p.phone'], ['data' => $this->t('Operations'), 'colspan' => '2']]; $query = $this->connection->select('phonebook', 'p')->extend('Drupal\\Core\\Database\\Query\\TableSortExtender')->extend('Drupal\\Core\\Database\\Query\\PagerSelectExtender'); $query->fields('p'); $result = $query->orderByHeader($header)->limit(25)->execute(); $output['table'] = ['#type' => 'table', '#header' => $header, '#empty' => $this->t('No entries found.')]; foreach ($result as $row) { $output['table'][] = [['data' => ['#markup' => $this->date_formatter->format($row->created)]], ['data' => ['#markup' => $this->date_formatter->formatTimeDiffSince($row->changed)]], ['data' => ['#markup' => $row->name]], ['data' => ['#markup' => $row->phone]], ['data' => ['#markup' => $this->l($this->t('edit'), new Url('d8phonebook.edit', ['phonebook' => $row->pbid]))]], ['data' => ['#markup' => $this->l($this->t('delete'), new Url('d8phonebook.delete', ['phonebook' => $row->pbid], ['query' => ['token' => $this->csrf_token_generator->get('phonebook/' . $row->pbid . '/delete')]]))]]]; } $output['pager'] = array('#type' => 'pager'); return $output; }
/** * @covers ::setCache */ public function testSetCacheAuthUser() { $form_build_id = 'the_form_build_id'; $form = []; $form_state = new FormState(); $cache_token = 'the_cache_token'; $form_data = $form; $form_data['#cache_token'] = $cache_token; $this->formCacheStore->expects($this->once()) ->method('setWithExpire') ->with($form_build_id, $form_data, $this->isType('int')); $form_state_data = $form_state->getCacheableArray(); $form_state_data['build_info']['safe_strings'] = []; $this->formStateCacheStore->expects($this->once()) ->method('setWithExpire') ->with($form_build_id, $form_state_data, $this->isType('int')); $this->csrfToken->expects($this->once()) ->method('get') ->willReturn($cache_token); $this->account->expects($this->once()) ->method('isAuthenticated') ->willReturn(TRUE); $this->formCache->setCache($form_build_id, $form, $form_state); }
/** * {@inheritdoc} */ public function setCache($form_build_id, $form, FormStateInterface $form_state) { // 6 hours cache life time for forms should be plenty. $expire = 21600; // Ensure that the form build_id embedded in the form structure is the same // as the one passed in as a parameter. This is an additional safety measure // to prevent legacy code operating directly with // \Drupal::formBuilder()->getCache() and \Drupal::formBuilder()->setCache() // from accidentally overwriting immutable form state. if (isset($form['#build_id']) && $form['#build_id'] != $form_build_id) { $this->logger->error('Form build-id mismatch detected while attempting to store a form in the cache.'); return; } // Cache form structure. if (isset($form)) { if ($this->currentUser->isAuthenticated()) { $form['#cache_token'] = $this->csrfToken->get(); } unset($form['#build_id_old']); $this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire); } if ($data = $form_state->getCacheableArray()) { $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire); } }
/** * Tests the access() method with no _controller_request attribute set. * * This will use the 'ALL' access conjunction. */ public function testAccessTokenMissAll() { $this->csrfToken->expects($this->never())->method('validate'); $route = new Route('/test-path', array(), array('_csrf_token' => 'TRUE'), array('_access_mode' => 'ALL')); $request = new Request(array('token' => 'test_query')); $this->assertSame(AccessInterface::ALLOW, $this->accessCheck->access($route, $request, $this->account)); }
/** * {@inheritdoc} */ public function setCache($form_build_id, $form, FormStateInterface $form_state) { // 6 hours cache life time for forms should be plenty. $expire = 21600; // Ensure that the form build_id embedded in the form structure is the same // as the one passed in as a parameter. This is an additional safety measure // to prevent legacy code operating directly with // \Drupal::formBuilder()->getCache() and \Drupal::formBuilder()->setCache() // from accidentally overwriting immutable form state. if (isset($form['#build_id']) && $form['#build_id'] != $form_build_id) { $this->logger->error('Form build-id mismatch detected while attempting to store a form in the cache.'); return; } // Cache form structure. if (isset($form)) { if ($this->currentUser->isAuthenticated()) { $form['#cache_token'] = $this->csrfToken->get(); } unset($form['#build_id_old']); $this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire); } // Cache form state. if ($this->configFactory->get('system.performance')->get('cache.page.use_internal') && $this->isPageCacheable()) { $form_state->addBuildInfo('immutable', TRUE); } // Store the known list of safe strings for form re-use. // @todo Ensure we are not storing an excessively large string list in: // https://www.drupal.org/node/2295823 $form_state->addBuildInfo('safe_strings', SafeMarkup::getAll()); if ($data = $form_state->getCacheableArray()) { $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire); } }
/** * Checks access based on a CSRF token for the request. * * @param \Symfony\Component\Routing\Route $route * The route to check against. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The route match object. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ public function access(Route $route, Request $request, RouteMatchInterface $route_match) { $parameters = $route_match->getRawParameters(); $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { $path = str_replace("{{$param}}", $value, $path); } if ($this->csrfToken->validate($request->query->get('token'), $path)) { $result = AccessResult::allowed(); } else { $result = AccessResult::forbidden(); } // Not cacheable because the CSRF token is highly dynamic. return $result->setCacheable(FALSE); }
/** * {@inheritdoc} */ public function validateForm($form_id, &$form, FormStateInterface &$form_state) { // If this form is flagged to always validate, ensure that previous runs of // validation are ignored. if ($form_state->isValidationEnforced()) { $form_state->setValidationComplete(FALSE); } // If this form has completed validation, do not validate again. if ($form_state->isValidationComplete()) { return; } // If the session token was set by self::prepareForm(), ensure that it // matches the current user's session. This is duplicate to code in // FormBuilder::doBuildForm() but left to protect any custom form handling // code. if (isset($form['#token'])) { if (!$this->csrfToken->validate($form_state->getValue('form_token'), $form['#token']) || $form_state->hasInvalidToken()) { $this->setInvalidTokenError($form_state); // Stop here and don't run any further validation handlers, because they // could invoke non-safe operations which opens the door for CSRF // vulnerabilities. $this->finalizeValidation($form, $form_state, $form_id); return; } } // Recursively validate each form element. $this->doValidateForm($form, $form_state, $form_id); $this->finalizeValidation($form, $form_state, $form_id); $this->handleErrorsWithLimitedValidation($form, $form_state, $form_id); }
/** * {@inheritdoc} */ public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) { if ($route->hasRequirement('_csrf_token')) { $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { $path = str_replace("{{$param}}", $value, $path); } // Adding this to the parameters means it will get merged into the query // string when the route is compiled. $parameters['token'] = $this->csrfToken->get($path); if ($cacheable_metadata) { // Tokens are per user and per session, so not cacheable. // @todo Improve in https://www.drupal.org/node/2351015. $cacheable_metadata->setCacheMaxAge(0); } } }
/** * Checks access based on a CSRF token for the request. * * @param \Symfony\Component\Routing\Route $route * The route to check against. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * * @return string * A \Drupal\Core\Access\AccessInterface constant value. */ public function access(Route $route, Request $request) { // If this is the controller request, check CSRF access as normal. if ($request->attributes->get('_controller_request')) { // @todo Remove dependency on the internal _system_path attribute: // https://www.drupal.org/node/2293501. return $this->csrfToken->validate($request->query->get('token'), $request->attributes->get('_system_path')) ? static::ALLOW : static::KILL; } // Otherwise, this could be another requested access check that we don't // want to check CSRF tokens on. $conjunction = $route->getOption('_access_mode') ?: 'ANY'; // Return ALLOW if all access checks are needed. if ($conjunction == 'ALL') { return static::ALLOW; } else { return static::DENY; } }
/** * @covers ::determineActiveTheme */ public function testDetermineActiveThemeDefaultTheme() { $theme = 'bartik'; // When the theme is the system default, an empty string is provided as the // theme token. See system_js_settings_alter(). $theme_token = ''; $request = new Request([], ['ajax_page_state' => ['theme' => $theme, 'theme_token' => $theme_token]]); $this->requestStack->push($request); $route_match = RouteMatch::createFromRequest($request); $this->tokenGenerator->validate(Argument::cetera())->shouldNotBeCalled(); $result = $this->negotiator->determineActiveTheme($route_match); $this->assertSame($theme, $result); }
/** * @covers ::validateForm */ public function testValidateValidFormToken() { $request_stack = new RequestStack(); $this->csrfToken->expects($this->once())->method('validate')->will($this->returnValue(TRUE)); $form_validator = $this->getMockBuilder('Drupal\\Core\\Form\\FormValidator')->setConstructorArgs([$request_stack, $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])->setMethods(array('doValidateForm'))->getMock(); $form_validator->expects($this->once())->method('doValidateForm'); $form['#token'] = 'test_form_id'; $form_state = $this->getMockBuilder('Drupal\\Core\\Form\\FormState')->setMethods(array('setErrorByName'))->getMock(); $form_state->expects($this->never())->method('setErrorByName'); $form_state->setValue('form_token', 'some_random_token'); $form_validator->validateForm('test_form_id', $form, $form_state); $this->assertTrue($form_state->isValidationComplete()); }
/** * Logs in a user. * * @param \Symfony\Component\HttpFoundation\Request $request * The request. * * @return \Symfony\Component\HttpFoundation\Response * A response which contains the ID and CSRF token. */ public function login(Request $request) { $format = $this->getRequestFormat($request); $content = $request->getContent(); $credentials = $this->serializer->decode($content, $format); if (!isset($credentials['name']) && !isset($credentials['pass'])) { throw new BadRequestHttpException('Missing credentials.'); } if (!isset($credentials['name'])) { throw new BadRequestHttpException('Missing credentials.name.'); } if (!isset($credentials['pass'])) { throw new BadRequestHttpException('Missing credentials.pass.'); } $this->floodControl($request, $credentials['name']); if ($this->userIsBlocked($credentials['name'])) { throw new BadRequestHttpException('The user has not been activated or is blocked.'); } if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) { $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name'])); /** @var \Drupal\user\UserInterface $user */ $user = $this->userStorage->load($uid); $this->userLoginFinalize($user); // Send basic metadata about the logged in user. $response_data = []; if ($user->get('uid')->access('view', $user)) { $response_data['current_user']['uid'] = $user->id(); } if ($user->get('roles')->access('view', $user)) { $response_data['current_user']['roles'] = $user->getRoles(); } if ($user->get('name')->access('view', $user)) { $response_data['current_user']['name'] = $user->getAccountName(); } $response_data['csrf_token'] = $this->csrfToken->get('rest'); $logout_route = $this->routeProvider->getRouteByName('user.logout.http'); // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck. $logout_path = ltrim($logout_route->getPath(), '/'); $response_data['logout_token'] = $this->csrfToken->get($logout_path); $encoded_response_data = $this->serializer->encode($response_data, $format); return new Response($encoded_response_data); } $flood_config = $this->config('user.flood'); if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) { $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier); } // Always register an IP-based failed login event. $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window')); throw new BadRequestHttpException('Sorry, unrecognized username or password.'); }
/** * Checks access. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param \Drupal\Core\Session\AccountInterface $account * The currently logged in account. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ public function access(Request $request, AccountInterface $account) { $method = $request->getMethod(); // This check only applies if // 1. this is a write operation // 2. the user was successfully authenticated and // 3. the request comes with a session cookie. if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE')) && $account->isAuthenticated() && $this->sessionConfiguration->hasSession($request)) { $csrf_token = $request->headers->get('X-CSRF-Token'); // @todo Remove validate call using 'rest' in 8.3. // Kept here for sessions active during update. if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY) && !$this->csrfToken->validate($csrf_token, 'rest')) { return AccessResult::forbidden()->setReason('X-CSRF-Token request header is missing')->setCacheMaxAge(0); } } // Let other access checkers decide if the request is legit. return AccessResult::allowed()->setCacheMaxAge(0); }
/** * {@inheritdoc} */ public function exportFormSubmit(array &$form, FormStateInterface $form_state) { // Redirect to the archive file download. $form_state->setRedirect('features.export_download', ['uri' => $this->archiveName, 'token' => $this->csrfToken->get($this->archiveName)]); }
/** * {@inheritdoc} */ public function create(array $batch) { // Ensure that a session is started before using the CSRF token generator. $this->session->start(); $this->connection->insert('batch')->fields(array('bid' => $batch['id'], 'timestamp' => REQUEST_TIME, 'token' => $this->csrfToken->get($batch['id']), 'batch' => serialize($batch)))->execute(); }
/** * #lazy_builder callback; gets a CSRF token for the given path. * * @param string $path * The path to get a CSRF token for. * * @return array * A renderable array representing the CSRF token. */ public function renderPlaceholderCsrfToken($path) { return ['#markup' => $this->csrfToken->get($path), '#cache' => ['contexts' => ['session']]]; }
/** * Tests the exception thrown when no 'hash_salt' is provided in settings. * * @covers ::get * @expectedException \RuntimeException */ public function testGetWithNoHashSalt() { // Update settings with no hash salt. new Settings(array()); $generator = new CsrfTokenGenerator($this->privateKey, $this->sessionMetadata); $generator->get(); }
/** * {@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 += array('#required' => FALSE, '#attributes' => array(), '#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'] = array(); } if (!isset($element['#id'])) { $unprocessed_id = 'edit-' . implode('-', $element['#parents']); $element['#id'] = Html::getUniqueId($unprocessed_id); // Provide a selector usable by JavaScript. As the ID is unique, its not // possible to rely on it in JavaScript. $element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id); } else { // Provide a selector usable by JavaScript. As the ID is unique, its not // possible to rely on it in JavaScript. $element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']); } // Add the aria-describedby attribute to associate the form control with its // description. if (!empty($element['#description'])) { $element['#attributes']['aria-describedby'] = $element['#id'] . '--description'; } // Handle input elements. if (!empty($element['#input'])) { $this->handleInputElement($form_id, $element, $form_state); } // Allow for elements to expand to multiple elements, e.g., radios, // checkboxes and files. if (isset($element['#process']) && !$element['#processed']) { foreach ($element['#process'] as $callback) { $complete_form =& $form_state->getCompleteForm(); $element = call_user_func_array($form_state->prepareCallback($callback), array(&$element, &$form_state, &$complete_form)); } $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 (array('#disabled', '#allow_focus') as $property) { if (isset($element[$property]) && !isset($element[$key][$property])) { $element[$key][$property] = $element[$property]; } } // Don't squash existing parents value. if (!isset($element[$key]['#parents'])) { // Check to see if a tree of child elements is present. If so, // continue down the tree if required. $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key); } // Ensure #array_parents follows the actual form structure. $array_parents = $element['#array_parents']; $array_parents[] = $key; $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), array($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; }