Commit 804d8792 authored by effulgentsia's avatar effulgentsia

Issue #2463567 by borisson_, Wim Leers, Xano: Push CSRF tokens for forms to...

Issue #2463567 by borisson_, Wim Leers, Xano: Push CSRF tokens for forms to placeholders + #lazy_builder
parent 13588e3a
......@@ -651,6 +651,27 @@ public function renderPlaceholderFormAction() {
];
}
/**
* #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}
*/
......@@ -725,15 +746,29 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
$form['#cache']['contexts'][] = 'user.roles:authenticated';
if ($user && $user->isAuthenticated()) {
// Generate a public token based on the form id.
$form['#token'] = $form_id;
// Generates a placeholder based on the form ID.
$placeholder = 'form_token_placeholder_' . hash('crc32b', $form_id);
$form['#token'] = $placeholder;
$form['form_token'] = array(
'#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
'#type' => 'token',
'#default_value' => $this->csrfToken->get($form['#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' => array('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,
],
......
......@@ -35,6 +35,7 @@ class BlockContentTranslationUITest extends ContentTranslationUITestBase {
*/
protected $defaultCacheContexts = [
'languages:language_interface',
'session',
'theme',
'url.path',
'url.query_args',
......
......@@ -11,6 +11,9 @@
use Drupal\Core\Session\UserSession;
use Drupal\comment\CommentInterface;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
/**
* Tests the bubbling up of comment cache tags when using the Comment list
......@@ -35,6 +38,16 @@ class CommentDefaultFormatterCacheTagsTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$session = new Session();
$request = Request::create('/');
$request->setSession($session);
/** @var RequestStack $stack */
$stack = $this->container->get('request_stack');
$stack->pop();
$stack->push($request);
// Set the current user to one that can access comments. Specifically, this
// user does not have access to the 'administer comments' permission, to
// ensure only published comments are visible to the end user.
......
......@@ -37,6 +37,7 @@ class CommentTranslationUITest extends ContentTranslationUITestBase {
*/
protected $defaultCacheContexts = [
'languages:language_interface',
'session',
'theme',
'timezone',
'url.query_args:_wrapper_format',
......
......@@ -31,6 +31,7 @@ class ContentTestTranslationUITest extends ContentTranslationUITestBase {
*/
protected $defaultCacheContexts = [
'languages:language_interface',
'session',
'theme',
'url.path',
'url.query_args',
......
......@@ -20,7 +20,7 @@ class MenuLinkContentTranslationUITest extends ContentTranslationUITestBase {
/**
* {inheritdoc}
*/
protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.path', 'url.query_args', 'user.permissions', 'user.roles:authenticated'];
protected $defaultCacheContexts = ['languages:language_interface', 'session', 'theme', 'url.path', 'url.query_args', 'user.permissions', 'user.roles:authenticated'];
/**
* Modules to enable.
......
......@@ -148,7 +148,7 @@ public function testRecentNodeBlock() {
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user', 'route']);
$this->drupalGet('node/add/article');
$this->assertText($label, 'Block was displayed on the node/add/article page.');
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.path', 'url.query_args', 'user', 'route']);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'session', 'theme', 'url.path', 'url.query_args', 'user', 'route']);
$this->drupalGet('node/' . $node1->id());
$this->assertText($label, 'Block was displayed on the node/N when node is of type article.');
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user', 'route', 'timezone']);
......
......@@ -26,6 +26,7 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
*/
protected $defaultCacheContexts = [
'languages:language_interface',
'session',
'theme',
'route',
'timezone',
......
......@@ -21,7 +21,7 @@ class ShortcutTranslationUITest extends ContentTranslationUITestBase {
/**
* {inheritdoc}
*/
protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.path', 'url.query_args', 'url.site'];
protected $defaultCacheContexts = ['languages:language_interface', 'session', 'theme', 'user', 'url.path', 'url.query_args', 'url.site'];
/**
* Modules to enable.
......
......@@ -783,10 +783,7 @@ public function testInvalidToken($expected, $valid_token, $user_is_authenticated
->willReturnArgument(0);
$this->csrfToken->expects($this->atLeastOnce())
->method('validate')
->will($this->returnValueMap([
[$form_token, $form_id, $valid_token],
[$form_id, $form_id, $valid_token],
]));
->willReturn($valid_token);
}
$current_user = $this->prophesize(AccountInterface::class);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment