-
Simple OAuth Token Handling with Cypress
Here’s a quick (and dirty?) way to handle requesting an access token and using it in a subsequent request.
You should probably pull the client_secret from an environment variable (not shown below).
commands.js
12345678910111213141516/*** Get an oAuth access token.*/Cypress.Commands.add('getOauthAccessToken', () => {cy.request({method: 'POST',url: '/oauth/token',form: true,body: {grant_type: 'client_credentials',client_id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',client_secret: 'xxxxxxxxxxxxxxxxxxxx',scope: '',},});});some-tests.spec.js
12345678910111213141516171819describe('API Foobar', function() {it('Gets expected values from GET /oauth/debug', function() {cy.getOauthAccessToken().then(oAuthResp => {cy.request({method: 'GET',url: '/oauth/debug?_format=json',headers: {'Authorization': `Bearer ${oAuthResp.body.access_token}`,},}).should((resp) => {expect(resp.status).to.eq(200);expect(resp.body.roles).to.deep.contain('authenticated');expect(resp.body.roles).to.deep.contain('api_foobar');});});});}); -
Remote PHP Debugging with Xdebug + PHPStorm or VSCode on Cloudways
Here’s a quick breakdown of the steps required to debug a PHP site on a remote Cloudways server.
Step 1:
Enable xdebug for the whole Cloudways server:
Server » Settings & Packages » Advanced » XDEBUG: Enabled
Step 2:
For the specific application in Cloudways, add some PHP settings:
Application » Application Settings » PHP FPM Settings:
123php_value[xdebug.mode] = debugphp_value[xdebug.start_with_request] = yesphp_value[xdebug.remote_connect_back] = 1Step 3: Setup SSH config for an ssh tunnel to the server:
- Edit
~/.ssh/config
1234Host mhd1_xdebugHostName 44.33.222.111User master_acevddddddRemoteForward 9003 localhost:9003Step 4 (PHPStorm):
Configure PHPStorm Preferences
- PHP » Debug » Xdebug » Debug port = 9003
- PHP » Debug » Xdebug — Check all four boxes:
- Can accept external connections
- Resolve breakpoint if it’s not available on the current line
- Force break at first line when no path mapping specified
- Force break at first line when a script is outside the project
- You shouldn’t need to configure servers, PHP cli, deployment locations, or anything similar…
Try It
- Start an SSH tunnel:
ssh mhd1_xdebug
- Drop a breakpoint in index.php (or whatever will surely execute)
- Visit the site, then wait for PHPStorm to prompt you for which file to connect to. Choose accordingly. It should connect and pause. You only have to do this once; it’ll remember the settings and path mapping in PHP » Servers.
Step 4 (VSCode)
Configure VSCode Preferences
- Install https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug
- Click the Run and Debug sidebar icon (⌘⇧D)
- SSH into the server to find the exact path to the root of the site (which should match the root of your repo locally, your workspace folder)
- Add new configuration (you can disable the log option if things work well right away)
123456789101112131415{"version": "0.2.0","configurations": [{"name": "Listen for Xdebug","type": "php","request": "launch","port": 9003,"pathMappings": {"/home/511111.cloudwaysapps.com/crwysaaaaa/public_html": "${workspaceFolder}",},"log": true},]}Try it
- Start an SSH tunnel:
ssh mhd1_xdebug
- Click Listen for Xdebug in the Run and Debug screen
- Drop a breakpoint in index.php (or whatever will surely execute)
- Visit the site
- Edit
-
Setting a Specific External Port for MySQL in Lando
Lando sets an external MySQL port dynamically by default. This means every time you (re)start a lando app it gets (potentially) a different external MySQL port. This is an annoyance as you have to change your port in TablePlus, SequelPro, or whatever MySQL GUI you’re using.
There is a simple fix: use a local lando file to override the forwarded MySQL port. These steps assume you already have a working .lando.yml file.
Step 1: Create a local lando file: .lando.local.yml
123services:database:portforward: 3307 <-- Specify an available port hereStep 2: Run lando rebuild if you’ve already started the site in the past
Step 3: Verify the settings by running lando info
1234567891011121314151617181920212223242526272829adam@acmbp sandbox % lando info[ { service: 'appserver',urls:[ 'https://localhost:50126','http://localhost:50127','http://sandbox.lndo.site/','https://sandbox.lndo.site/' ],type: 'php',healthy: true,via: 'apache',webroot: '.',config: {},version: '7.3',meUser: 'www-data',hasCerts: true,hostnames: [ 'appserver.sandbox.internal' ] },{ service: 'database',urls: [],type: 'mysql',healthy: true,internal_connection: { host: 'database', port: '3306' },external_connection: { host: '127.0.0.1', port: '3307' },healthcheck: 'bash -c "[ -f /bitnami/mysql/.mysql_initialized ]"',creds: { database: 'main_db', password: 'main_pass', user: 'main_user' },config: {},version: '5.7',meUser: 'www-data',hasCerts: false,hostnames: [ 'database.sandbox.internal' ] } ]Step 4: Add .lando.local.yml to your .gitignore file
This site will always get the port you specified if it’s available when you start the app. You can use this port in your MySQL gui, scripts, etc.
-
Setting a LIMIT on an UPDATE query in Drupal 8 / 9
Here’s a quick tip regarding Drupal database manipulation. Specifically, I needed a way to flip a boolean value (from 1 to 0) on the first N rows in a database table that matched a specific set of conditions.
The Problem
It seems you cannot enforce a LIMIT on an UPDATE query using a static query in Drupal.
Here’s what I was trying to do:
1234567$db = \Drupal::database();$query = $db->query('UPDATE {mytable} SET ondemand=:zero WHERE ondemand=:one ORDER BY imported_at ASC LIMIT :limit', [':zero' => 0,':one' => 1,':limit' => $qtyToConvert,]);$query->execute();This throws an error. I believe the issue is caused by the variable substitution wrapping the limit value in quotes. The error message starts with:
1SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''10'' at line 1...If you run this query manually in MySQL, it works fine with
LIMIT 10
but doesn’t work withLIMIT '10'
(it throws the same error shown above).The Solution
Given dynamic queries are favored over static queries, and that this static query doesn’t work, I ended up getting to the finish line with two dynamic queries.
I have tested the solution on 10,000 records and it took < 6 seconds to process the entire request on my local dev lando site.
“code” is a unique identifier column in the database table.
12345678910111213$db = \Drupal::database();$query = $db->select('mytable', 'mt')->condition('ondemand', 1)->orderBy('imported_at')->fields('mt', ['code'])->range(0, $qtyToConvert);$codesToReset = $query->execute()->fetchCol();$db->update('mytable')->fields(['ondemand' => 0])->condition('code', $codesToReset, 'IN')->execute();return count($codesToReset); -
Missing Titles on Drupal 8 ECK Content Listings
UPDATE: I discovered the solution below was only working when the Title base field was enabled for the entity type. If you enable the title field on the entity type it works, but then the title is required to save a new record. I will look for an alternative solution to avoid using the Title field altogether. Also, I’ll look into https://www.drupal.org/project/eck/issues/2785297.
I’ve recently run into an issue with an ECK-created custom entity type. There was no reason for me to use the “Title” field on this particular entity type. Unfortunately, when you don’t have a title field several of the built-in entity listings and displays seem broken. Here’s what the Content List screen looks like, for example:
-
Adding Custom Fields to the Field Edit Screen in Drupal 8 / 9
Introduction
I’m building an API with Drupal 8/9. There is additional information I want to track against each field instance in each content type (or custom entity type) in the system. This information includes where the field’s data comes from, who is responsible for maintaining it, etc. This is useful for the site administrator and developers to track where things are coming from. Drupal’s Third Party Settings functionality makes this easy.
Result
This is what you see when you edit the First name field:
When you click Save settings the data is saved to this specific field’s instance configuration (meaning if you reuse this field you can fill the info out differently per instance).
Solution
There are two ways to achieve this. In both cases, we use hook_form_FORM_ID_alter() to introduce the new fields. I am doing this within a custom module called pdms. The code lives in pdms.module.
Method #1:
With this method, the system automatically handles storing the value into the field configuration’s third party settings.
This happens because we use the third_party_settings key.
123456789101112131415/*** Implements hook_form_FORM_ID_alter().*/function pdms_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {$entity = $form_state->getFormObject()->getEntity();$form['third_party_settings']['pdms']['canonical_source'] = ['#type' => 'textarea','#title' => t('Canonical source'),'#description' => t('From where does this data originate?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'canonical_source'),];... etc ...}The issue I had with this approach is that I could not get it to work if I put the fields in a fieldset (as shown in the screenshot).
I think it’d be a matter of getting $entity->getThirdPartySetting() to look into the fieldset somehow. If you know how to do this, please let me know!
Method #2:
This method gives us more control over what happens when the form is submitted.
Because we have more control we’re able to traverse the fieldset easily.
You can see, too, how I unset the third party setting if the value is empty. I am not sure if I’ll keep this in place; we’ll see if it causes any issues when I attempt to build a report showing all of the fields and their info.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869/*** Implements hook_form_FORM_ID_alter().*/function pdms_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {$entity = $form_state->getFormObject()->getEntity();$form['#entity_builders'][] = 'pdms_set_third_party_settings_field_info';$form['pdms_field_info'] = ['#type' => 'fieldset','#title' => 'MySite Field Info',];$form['pdms_field_info']['canonical_source'] = ['#type' => 'textfield','#title' => t('Canonical source'),'#description' => t('From where does this data originate?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'canonical_source'),];$form['pdms_field_info']['canonical_source_location'] = ['#type' => 'textfield','#title' => t('Canonical source location'),'#description' => t('Where within the canonical source can we find this data?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'canonical_source_location'),];$form['pdms_field_info']['author'] = ['#type' => 'textfield','#title' => t('Author'),'#description' => t('Who maintains this data at the canonical source?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'author'),];$form['pdms_field_info']['author_contact_info'] = ['#type' => 'textfield','#title' => t('Author contact info'),'#description' => t('How do we contact the author?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'author_contact_info'),];$form['pdms_field_info']['known_usages'] = ['#type' => 'textarea','#title' => t('Known usages'),'#description' => t('Who/what is using this data and how are they using it?<br/>Separate each item with a blank line.<br/><strong>Format:</strong> source: description</strong>'),'#default_value' => $entity->getThirdPartySetting('pdms', 'known_usages'),];}/*** Entity builder for PDMS field info third party settings.*/function pdms_set_third_party_settings_field_info($entity_type, $entity, &$form, \Drupal\Core\Form\FormStateInterface $form_state) {$fields = ['canonical_source','canonical_source_location','author','author_contact_info','known_usages',];foreach ($fields as $field_name) {if ($form_state->getValue($field_name)) {$entity->setThirdPartySetting('pdms', $field_name, $form_state->getValue($field_name));}else {$entity->unsetThirdPartySetting('pdms', $field_name);}} -
Remotely Resetting a Pantheon WordPress User’s Password via Terminus and WP-CLI
1terminus wp SITEHERE.ENVHERE -- user update UIDHERE --user_pass=PASSHERE -
Increasing Memory for Specific Paths in Drupal 8
Most of the examples I see for Drupal 8 are for single-path memory limit increases.
The examples for Drupal 7 often follow the pattern below.
Here’s a D8 version that supports multiple paths.
Place this in your settings.php (or site/environment-specific settings.php), then update the paths accordingly.
-
Conditionally Triggering Salesforce Push Operations in Drupal 8
The user interface for the Salesforce Suite in Drupal 8 is fantastic. The suite itself is incredibly powerful. Among the many options, for each mapping you create you can specify what operations should trigger a synchronization. Here’s an example:
We recently faced an issue with the “Drupal entity update” trigger being called too often. The salesforce_push module implements hook_entity_update() , which gets called a lot. After looking at how the functions are called in salesforce_push.module I realized there were two choices:
-
Patch for Drupal 8.7.14 SA-CORE-2020-004, SA-CORE-2020-005, and SA-CORE-2020-006
I have a site that’s temporarily stuck on 8.7.14; it’s not worth the risk to update to 8.8.8 right now.
I was able to diff 8.8.7 and 8.8.8 to figure out what changes were made for these security announcements:
- Drupal core – Critical – Cross-Site Request Forgery – SA-CORE-2020-004
- Drupal core – Critical – Arbitrary PHP code execution – SA-CORE-2020-005
- Drupal core – Less critical – Access bypass – SA-CORE-2020-006
I could then compare the changes with 8.7.14 to create the patch below, which applies against 8.7.14 without any issues.
DISCLAIMER: The formal recommendation is to update to 8.8.8 if at all possible. Use the patch below at your own risk.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.incindex 348724d6..e9b7cbc1 100644--- a/core/includes/bootstrap.inc+++ b/core/includes/bootstrap.inc@@ -673,11 +673,17 @@ function drupal_valid_test_ua($new_prefix = NULL) {// Ensure that no information leaks on production sites.$test_db = new TestDatabase($prefix);$key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';- if (!is_readable($key_file)) {+ if (!is_readable($key_file) || is_dir($key_file)) {header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');exit;}$private_key = file_get_contents($key_file);+ // The string from drupal_generate_test_ua() is 74 bytes long. If we don't+ // have it, tests cannot be allowed.+ if (empty($private_key) || strlen($private_key) < 74) {+ header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');+ exit;+ }// The file properties add more entropy not easily accessible to others.$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);$time_diff = REQUEST_TIME - $time;diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.phpindex 4bf64196..7bacc473 100644--- a/core/lib/Drupal/Core/Form/FormBuilder.php+++ b/core/lib/Drupal/Core/Form/FormBuilder.php@@ -18,6 +18,7 @@use Drupal\Core\Theme\ThemeManagerInterface;use Symfony\Component\EventDispatcher\EventDispatcherInterface;use Symfony\Component\HttpFoundation\FileBag;+use Symfony\Component\HttpFoundation\ParameterBag;use Symfony\Component\HttpFoundation\RequestStack;use Symfony\Component\HttpFoundation\Response;@@ -955,8 +956,16 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state// This value is checked in self::handleInputElement().$form_state->setInvalidToken(TRUE);+ // Ignore all submitted values.+ $form_state->setUserInput([]);++ $request = $this->requestStack->getCurrentRequest();+ // Do not trust any POST data.+ $request->request = new ParameterBag();// Make sure file uploads do not get processed.- $this->requestStack->getCurrentRequest()->files = new FileBag();+ $request->files = new FileBag();+ // Ensure PHP globals reflect these changes.+ $request->overrideGlobals();}}}diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.phpindex 57d24cc2..2afcb52b 100644--- a/core/lib/Drupal/Core/Form/FormValidator.php+++ b/core/lib/Drupal/Core/Form/FormValidator.php@@ -124,10 +124,8 @@ public function validateForm($form_id, &$form, FormStateInterface &$form_state)* {@inheritdoc}*/public function setInvalidTokenError(FormStateInterface $form_state) {- $url = $this->requestStack->getCurrentRequest()->getRequestUri();-// Setting this error will cause the form to fail validation.- $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href=":link">reload this page</a>.', [':link' => $url]));+ $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.'));}/**diff --git a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.phpindex 0b6254d8..db8e7fbe 100644--- a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php+++ b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php@@ -45,7 +45,7 @@ public function testManagedFile() {$file_field_name => \Drupal::service('file_system')->realpath($test_file->getFileUri()),];$this->drupalPostForm(NULL, $edit, t('Save'));- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');+ $this->assertText('The form has become outdated.');$last_fid = $this->getLastFileId();$this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.');diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.phpindex 5c43d3ee..e21903c2 100644--- a/core/modules/jsonapi/src/Controller/EntityResource.php+++ b/core/modules/jsonapi/src/Controller/EntityResource.php@@ -315,7 +315,7 @@ public function patchIndividual(ResourceType $resource_type, EntityInterface $en));}$data += ['attributes' => [], 'relationships' => []];- $field_names = array_merge(array_keys($data['attributes']), array_keys($data['relationships']));+ $field_names = array_map([$resource_type, 'getInternalName'], array_merge(array_keys($data['attributes']), array_keys($data['relationships'])));array_reduce($field_names, function (EntityInterface $destination, $field_name) use ($resource_type, $parsed_entity) {$this->updateEntityField($resource_type, $parsed_entity, $destination, $field_name);diff --git a/core/modules/jsonapi/src/Controller/FileUpload.php b/core/modules/jsonapi/src/Controller/FileUpload.phpindex b07dd51d..f8517fd7 100644--- a/core/modules/jsonapi/src/Controller/FileUpload.php+++ b/core/modules/jsonapi/src/Controller/FileUpload.php@@ -112,6 +112,7 @@ public function __construct(AccountInterface $current_user, EntityFieldManagerIn* created file entity.*/public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) {+ $file_field_name = $resource_type->getInternalName($file_field_name);$field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity);@@ -138,7 +139,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy$entity->save();$route_parameters = ['entity' => $entity->uuid()];- $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $file_field_name);+ $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $resource_type->getPublicName($file_field_name));$related_url = Url::fromRoute($route_name, $route_parameters)->toString(TRUE);$request = Request::create($related_url->getGeneratedUrl(), 'GET', [], $request->cookies->all(), [], $request->server->all());return $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);@@ -161,6 +162,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy* Thrown when there are validation errors.*/public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) {+ $file_field_name = $resource_type->getInternalName($file_field_name);$field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);static::ensureFileUploadAccess($this->currentUser, $field_definition);@@ -182,7 +184,7 @@ public function handleFileUploadForNewResource(Request $request, ResourceType $r/* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */$links = new LinkCollection(['self' => $self_link]);- $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($file_field_name);+ $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name));$file_resource_type = reset($relatable_resource_types);$resource_object = ResourceObject::createFromEntity($file_resource_type, $file);return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []);diff --git a/core/modules/system/tests/src/Functional/Form/FormTest.php b/core/modules/system/tests/src/Functional/Form/FormTest.phpindex 849944b0..62ae3c54 100644--- a/core/modules/system/tests/src/Functional/Form/FormTest.php+++ b/core/modules/system/tests/src/Functional/Form/FormTest.php@@ -244,21 +244,27 @@ public function testInputWithInvalidToken() {$this->assertSession()->elementExists('css', 'input[name="form_token"]')->setValue('invalid token');+ $random_string = $this->randomString();$edit = [- 'textfield' => $this->randomString(),+ 'textfield' => $random_string,'checkboxes[bar]' => TRUE,'select' => 'bar','radios' => 'foo',];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- // Verify that input elements retained the posted values.- $this->assertFieldByName('textfield', $edit['textfield']);++ $assert = $this->assertSession();+ $element = $assert->fieldExists('textfield');+ $this->assertEmpty($element->getValue());+ $assert->responseNotContains($random_string);+ $this->assertText('The form has become outdated.');+ // Ensure that we don't use the posted values.+ $this->assertFieldByName('textfield', '');$this->assertNoFieldChecked('edit-checkboxes-foo');- $this->assertFieldChecked('edit-checkboxes-bar');- $this->assertOptionSelected('edit-select', 'bar');- $this->assertFieldChecked('edit-radios-foo');+ $this->assertNoFieldChecked('edit-checkboxes-bar');+ $this->assertOptionSelected('edit-select', '');+ $this->assertNoFieldChecked('edit-radios-foo');// Check another form that has a textarea input.$this->drupalGet(Url::fromRoute('form_test.required'));@@ -271,9 +277,9 @@ public function testInputWithInvalidToken() {];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('textfield', $edit['textfield']);- $this->assertFieldByName('textarea', $edit['textarea']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('textfield', '');+ $this->assertFieldByName('textarea', '');// Check another form that has a number input.$this->drupalGet(Url::fromRoute('form_test.number'));@@ -281,12 +287,14 @@ public function testInputWithInvalidToken() {->elementExists('css', 'input[name="form_token"]')->setValue('invalid token');$edit = [- 'integer_step' => mt_rand(1, 100),+ // We choose a random value which is higher than the default value,+ // so we don't accidentally generate the default value.+ 'integer_step' => mt_rand(6, 100),];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('integer_step', $edit['integer_step']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('integer_step', 5);// Check a form with a Url field$this->drupalGet(Url::fromRoute('form_test.url'));@@ -298,8 +306,9 @@ public function testInputWithInvalidToken() {];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('url', $edit['url']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('url', '');+}/**diff --git a/core/modules/system/tests/src/Functional/Form/ValidationTest.php b/core/modules/system/tests/src/Functional/Form/ValidationTest.phpindex 094a0af7..50451a98 100644--- a/core/modules/system/tests/src/Functional/Form/ValidationTest.php+++ b/core/modules/system/tests/src/Functional/Form/ValidationTest.php@@ -68,7 +68,7 @@ public function testValidate() {$this->drupalPostForm(NULL, ['name' => 'validate'], 'Save');$this->assertNoFieldByName('name', '#value changed by #validate', 'Form element #value was not altered.');$this->assertNoText('Name value: value changed by setValueForElement() in #validate', 'Form element value in $form_state was not altered.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');+ $this->assertText('The form has become outdated.');}/**diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.phpindex 8d20db5c..d2dec0c6 100644--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php@@ -799,12 +799,30 @@ public function testInvalidToken($expected, $valid_token, $user_is_authenticated$expected_form = $form_id();$form_arg = $this->getMockForm($form_id, $expected_form);+ // Set up some request data so we can be sure it is removed when a token is+ // invalid.+ $this->request->request->set('foo', 'bar');+ $_POST['foo'] = 'bar';+$form_state = new FormState();$input['form_id'] = $form_id;$input['form_token'] = $form_token;+ $input['test'] = 'example-value';$form_state->setUserInput($input);- $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);+ $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);$this->assertSame($expected, $form_state->hasInvalidToken());+ if ($expected) {+ $this->assertEmpty($form['test']['#value']);+ $this->assertEmpty($form_state->getValue('test'));+ $this->assertEmpty($_POST);+ $this->assertEmpty(iterator_to_array($this->request->request->getIterator()));+ }+ else {+ $this->assertEquals('example-value', $form['test']['#value']);+ $this->assertEquals('example-value', $form_state->getValue('test'));+ $this->assertEquals('bar', $_POST['foo']);+ $this->assertEquals('bar', $this->request->request->get('foo'));+ }}public function providerTestInvalidToken() {diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.phpindex ef08ab77..36605065 100644--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php@@ -173,7 +173,7 @@ protected function setUp() {->getMock();$this->account = $this->getMock('Drupal\Core\Session\AccountInterface');$this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');- $this->request = new Request();+ $this->request = Request::createFromGlobals();$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');$this->requestStack = new RequestStack();$this->requestStack->push($this->request);diff --git a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.phpindex 18f0be28..10f54905 100644--- a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php+++ b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php@@ -131,7 +131,7 @@ public function testValidateInvalidFormToken() {->getMock();$form_state->expects($this->once())->method('setErrorByName')- ->with('form_token', 'The form has become outdated. Copy any unsaved work in the form below and then <a href="/test/example?foo=bar">reload this page</a>.');+ ->with('form_token', 'The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.');$form_state->setValue('form_token', 'some_random_token');$form_validator->validateForm('test_form_id', $form, $form_state);$this->assertTrue($form_state->isValidationComplete());