Commit e242632c authored by catch's avatar catch

Issue #2450897 by plach, epari.siva, dawehner, jibran, Fabianx: Cache Field views row output

parent db8fda5c
......@@ -295,7 +295,7 @@ protected function createCacheID(array $elements) {
* {@inheritdoc}
*/
public function getCacheableRenderArray(array $elements) {
return [
$data = [
'#markup' => $elements['#markup'],
'#attached' => $elements['#attached'],
'#post_render_cache' => $elements['#post_render_cache'],
......@@ -305,6 +305,28 @@ public function getCacheableRenderArray(array $elements) {
'max-age' => $elements['#cache']['max-age'],
],
];
// Preserve cacheable items if specified. If we are preserving any cacheable
// children of the element, we assume we are only interested in their
// individual markup and not the parent's one, thus we empty it to minimize
// the cache entry size.
if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) {
$data['#cache_properties'] = $elements['#cache_properties'];
// Extract all the cacheable items from the element using cache
// properties.
$cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties']));
$cacheable_children = Element::children($cacheable_items);
if ($cacheable_children) {
$data['#markup'] = '';
// Cache only cacheable children's markup.
foreach ($cacheable_children as $key) {
$cacheable_items[$key] = ['#markup' => $cacheable_items[$key]['#markup']];
}
}
$data += $cacheable_items;
}
return $data;
}
}
......@@ -9,12 +9,11 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Component\Utility\SafeMarkup;
/**
* Turns a render array into a HTML string.
......@@ -180,7 +179,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
if ($is_root_call) {
$this->processPostRenderCache($elements);
}
// Mark the element markup as safe. If we have cached children, we need
// to mark them as safe too. The parent markup contains the child
// markup, so if the parent markup is safe, then the markup of the
// individual children must be safe as well.
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
if (!empty($elements['#cache_properties'])) {
foreach (Element::children($cached_element) as $key) {
SafeMarkup::set($cached_element[$key]['#markup']);
}
}
// The render cache item contains all the bubbleable rendering metadata
// for the subtree.
$this->updateStack($elements);
......
......@@ -7,8 +7,6 @@
namespace Drupal\Core\Render;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Defines an interface for turning a render array into a string.
*/
......@@ -249,6 +247,12 @@ public function renderPlain(&$elements);
* and written to cache using the value of $pre_bubbling_cid as the cache
* ID. This ensures the pre-bubbling ("wrong") cache ID redirects to the
* post-bubbling ("right") cache ID.
* - If this element also has #cache_properties defined, all the array items
* matching the specified property names will be cached along with the
* element markup. If properties include children names, the system
* assumes only children's individual markup is relevant and ignores the
* parent markup. This approach is normally not needed and should be
* adopted only when dealing with very advanced use cases.
* - If this element has an array of #post_render_cache functions defined,
* or any of its children has (which we would know thanks to the stack
* having been updated just before the render caching step), they are
......
......@@ -152,6 +152,8 @@ public function testUsername() {
$account_switcher->switchTo(new AnonymousUserSession());
$executable = Views::getView($view_id);
$executable->storage->invalidateCaches();
$build = $executable->preview();
$this->setRawContent($renderer->render($build));
......
......@@ -62,16 +62,17 @@ public function access(UserInterface $user, AccountInterface $account) {
// Anonymous users cannot have contact forms.
if ($contact_account->isAnonymous()) {
return AccessResult::forbidden()->cachePerPermissions();
return AccessResult::forbidden();
}
// Users may not contact themselves.
// Users may not contact themselves by default, hence this requires user
// granularity for caching.
$access = AccessResult::neutral()->cachePerUser();
if ($account->id() == $contact_account->id()) {
return AccessResult::forbidden()->cachePerUser();
return $access;
}
// User administrators should always have access to personal contact forms.
$access = AccessResult::neutral()->cachePerPermissions();
$permission_access = AccessResult::allowedIfHasPermission($account, 'administer users');
if ($permission_access->isAllowed()) {
return $access->orIf($permission_access);
......@@ -83,14 +84,12 @@ public function access(UserInterface $user, AccountInterface $account) {
return $access;
}
// Load preference of the requested user.
// Forbid access if the requested user has disabled their contact form.
$account_data = $this->userData->get('contact', $contact_account->id(), 'enabled');
if (isset($account_data)) {
// Forbid access if the requested user has disabled their contact form.
if (empty($account_data)) {
return $access;
}
if (isset($account_data) && !$account_data) {
return $access;
}
// If the requested user did not save a preference yet, deny access if the
// configured default is disabled.
$contact_settings = $this->configFactory->get('contact.settings');
......
......@@ -152,13 +152,13 @@ function testPersonalContactAccess() {
// Test that anonymous users can access admin user's contact form.
$this->drupalGet('user/' . $this->adminUser->id() . '/contact');
$this->assertResponse(200);
$this->assertCacheContext('user.permissions');
$this->assertCacheContext('user');
// Revoke the personal contact permission for the anonymous user.
user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access user contact forms'));
$this->drupalGet('user/' . $this->contactUser->id() . '/contact');
$this->assertResponse(403);
$this->assertCacheContext('user.permissions');
$this->assertCacheContext('user');
$this->drupalGet('user/' . $this->adminUser->id() . '/contact');
$this->assertResponse(403);
......
......@@ -7,6 +7,7 @@
namespace Drupal\contact\Tests\Views;
use Drupal\Core\Cache\Cache;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\user\Entity\User;
......@@ -84,6 +85,8 @@ public function testContactLink() {
// Disable contact link for no_contact.
$this->userData->set('contact', $no_contact_account->id(), 'enabled', FALSE);
// @todo Remove cache invalidation in https://www.drupal.org/node/2477903.
Cache::invalidateTags($no_contact_account->getCacheTags());
$this->drupalGet('test-contact-link');
$this->assertContactLinks($accounts, array('root', 'admin'));
}
......
......@@ -3,12 +3,13 @@ status: true
dependencies:
module:
- contact
- user
id: test_contact_link
label: test_contact_link
module: views
description: ''
tag: ''
base_table: users
base_table: users_field_data
base_field: uid
core: 8.x
display:
......@@ -69,7 +70,7 @@ display:
fields:
name:
id: name
table: users
table: users_field_data
field: name
label: ''
alter:
......@@ -111,7 +112,7 @@ display:
filters:
status:
value: true
table: users
table: users_field_data
field: status
id: status
expose:
......@@ -127,10 +128,22 @@ display:
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
cacheable: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-contact-link
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
cacheable: false
......@@ -100,6 +100,7 @@ public function testIntegration() {
// Disable replacing variables and check that the tokens aren't replaced.
$view->destroy();
$view->storage->invalidateCaches();
$view->initHandlers();
$this->executeView($view);
$view->initStyle();
......
......@@ -117,6 +117,7 @@ protected function setUp() {
* The view to add field data to.
*/
protected function prepareView(ViewExecutable $view) {
$view->storage->invalidateCaches();
$view->initDisplay();
foreach ($this->fieldStorages as $field_storage) {
$field_name = $field_storage->getName();
......
<?php
/**
* @file
* Contains \Drupal\simpletest\UserCreationTrait.
*/
namespace Drupal\simpletest;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
/**
* Provides methods to create additional test users and switch the currently
* logged in one.
*
* This trait is meant to be used only by test classes extending
* \Drupal\simpletest\TestBase.
*/
trait UserCreationTrait {
/**
* Switch the current logged in user.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user account object.
*/
protected function setCurrentUser(AccountInterface $account) {
\Drupal::currentUser()->setAccount($account);
}
/**
* Create a user with a given set of permissions.
*
* @param array $permissions
* Array of permission names to assign to user. Note that the user always
* has the default permissions derived from the "authenticated users" role.
* @param string $name
* The user name.
* @param bool $admin
* (optional) Whether the user should be an administrator
* with all the available permissions.
*
* @return \Drupal\user\Entity\User|false
* A fully loaded user object with pass_raw property, or FALSE if account
* creation fails.
*/
protected function createUser(array $permissions = array(), $name = NULL, $admin = FALSE) {
// Create a role with the given permission set, if any.
$rid = FALSE;
if ($permissions) {
$rid = $this->createRole($permissions);
if (!$rid) {
return FALSE;
}
}
// Create a user assigned to that role.
$edit = array();
$edit['name'] = !empty($name) ? $name : $this->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass'] = user_password();
$edit['status'] = 1;
if ($rid) {
$edit['roles'] = array($rid);
}
if ($admin) {
$edit['roles'][] = $this->createAdminRole();
}
$account = User::create($edit);
$account->save();
$this->assertTrue($account->id(), SafeMarkup::format('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), 'User login');
if (!$account->id()) {
return FALSE;
}
// Add the raw password so that we can log in as this user.
$account->pass_raw = $edit['pass'];
return $account;
}
/**
* Creates an administrative role.
*
* @param string $rid
* (optional) The role ID (machine name). Defaults to a random name.
* @param string $name
* (optional) The label for the role. Defaults to a random string.
* @param integer $weight
* (optional) The weight for the role. Defaults NULL so that entity_create()
* sets the weight to maximum + 1.
*
* @return string
* Role ID of newly created role, or FALSE if role creation failed.
*/
protected function createAdminRole($rid = NULL, $name = NULL, $weight = NULL) {
$rid = $this->createRole([], $rid, $name, $weight);
if ($rid) {
/** @var \Drupal\user\RoleInterface $role */
$role = Role::load($rid);
$role->setIsAdmin(TRUE);
$role->save();
}
return $rid;
}
/**
* Creates a role with specified permissions.
*
* @param array $permissions
* Array of permission names to assign to role.
* @param string $rid
* (optional) The role ID (machine name). Defaults to a random name.
* @param string $name
* (optional) The label for the role. Defaults to a random string.
* @param integer $weight
* (optional) The weight for the role. Defaults NULL so that entity_create()
* sets the weight to maximum + 1.
*
* @return string
* Role ID of newly created role, or FALSE if role creation failed.
*/
protected function createRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
// Generate a random, lowercase machine name if none was passed.
if (!isset($rid)) {
$rid = strtolower($this->randomMachineName(8));
}
// Generate a random label.
if (!isset($name)) {
// In the role UI role names are trimmed and random string can start or
// end with a space.
$name = trim($this->randomString(8));
}
// Check the all the permissions strings are valid.
if (!$this->checkPermissions($permissions)) {
return FALSE;
}
// Create new role.
$role = Role::create(array(
'id' => $rid,
'label' => $name,
));
if (isset($weight)) {
$role->set('weight', $weight);
}
$result = $role->save();
$this->assertIdentical($result, SAVED_NEW, SafeMarkup::format('Created role ID @rid with name @name.', array(
'@name' => var_export($role->label(), TRUE),
'@rid' => var_export($role->id(), TRUE),
)), 'Role');
if ($result === SAVED_NEW) {
// Grant the specified permissions to the role, if any.
if (!empty($permissions)) {
$this->grantPermissions($role, $permissions);
$assigned_permissions = Role::load($role->id())->getPermissions();
$missing_permissions = array_diff($permissions, $assigned_permissions);
if (!$missing_permissions) {
$this->pass(SafeMarkup::format('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), 'Role');
}
else {
$this->fail(SafeMarkup::format('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions))), 'Role');
}
}
return $role->id();
}
else {
return FALSE;
}
}
/**
* Checks whether a given list of permission names is valid.
*
* @param array $permissions
* The permission names to check.
*
* @return bool
* TRUE if the permissions are valid, FALSE otherwise.
*/
protected function checkPermissions(array $permissions) {
$available = array_keys(\Drupal::service('user.permissions')->getPermissions());
$valid = TRUE;
foreach ($permissions as $permission) {
if (!in_array($permission, $available)) {
$this->fail(SafeMarkup::format('Invalid permission %permission.', array('%permission' => $permission)), 'Role');
$valid = FALSE;
}
}
return $valid;
}
/**
* Grant permissions to a user role.
*
* @param \Drupal\user\RoleInterface $role
* The ID of a user role to alter.
* @param array $permissions
* (optional) A list of permission names to grant.
*/
protected function grantPermissions(RoleInterface $role, array $permissions) {
foreach ($permissions as $permission) {
$role->grantPermission($permission);
}
$role->trustData()->save();
}
}
......@@ -7,32 +7,26 @@
namespace Drupal\simpletest;
use Drupal\block\Entity\Block;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\DependencyInjection\YamlFileLoader;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\block\Entity\Block;
use Drupal\node\Entity\NodeType;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Symfony\Component\HttpFoundation\Request;
use Drupal\user\Entity\Role;
/**
* Test case for typical Drupal tests.
......@@ -43,6 +37,12 @@ abstract class WebTestBase extends TestBase {
use AssertContentTrait;
use UserCreationTrait {
createUser as drupalCreateUser;
createRole as drupalCreateRole;
createAdminRole as drupalCreateAdminRole;
}
/**
* The profile to install as a basis for testing.
*
......@@ -520,141 +520,6 @@ protected function drupalCompareFiles($file1, $file2) {
}
}
/**
* Create a user with a given set of permissions.
*
* @param array $permissions
* Array of permission names to assign to user. Note that the user always
* has the default permissions derived from the "authenticated users" role.
* @param string $name
* The user name.
*
* @return \Drupal\user\Entity\User|false
* A fully loaded user object with pass_raw property, or FALSE if account
* creation fails.
*/
protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
// Create a role with the given permission set, if any.
$rid = FALSE;
if ($permissions) {
$rid = $this->drupalCreateRole($permissions);
if (!$rid) {
return FALSE;
}
}
// Create a user assigned to that role.
$edit = array();
$edit['name'] = !empty($name) ? $name : $this->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass'] = user_password();
$edit['status'] = 1;
if ($rid) {
$edit['roles'] = array($rid);
}
$account = entity_create('user', $edit);
$account->save();
$this->assertTrue($account->id(), SafeMarkup::format('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), 'User login');
if (!$account->id()) {
return FALSE;
}
// Add the raw password so that we can log in as this user.
$account->pass_raw = $edit['pass'];
return $account;
}
/**
* Creates a role with specified permissions.
*
* @param array $permissions
* Array of permission names to assign to role.
* @param string $rid
* (optional) The role ID (machine name). Defaults to a random name.
* @param string $name
* (optional) The label for the role. Defaults to a random string.
* @param integer $weight
* (optional) The weight for the role. Defaults NULL so that entity_create()
* sets the weight to maximum + 1.
*
* @return string
* Role ID of newly created role, or FALSE if role creation failed.
*/
protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
// Generate a random, lowercase machine name if none was passed.
if (!isset($rid)) {
$rid = strtolower($this->randomMachineName(8));
}
// Generate a random label.
if (!isset($name)) {
// In the role UI role names are trimmed and random string can start or
// end with a space.
$name = trim($this->randomString(8));
}
// Check the all the permissions strings are valid.
if (!$this->checkPermissions($permissions)) {
return FALSE;
}
// Create new role.
$role = entity_create('user_role', array(
'id' => $rid,
'label' => $name,
));
if (!is_null($weight)) {
$role->set('weight', $weight);
}
$result = $role->save();
$this->assertIdentical($result, SAVED_NEW, SafeMarkup::format('Created role ID @rid with name @name.', array(
'@name' => var_export($role->label(), TRUE),
'@rid' => var_export($role->id(), TRUE),
)), 'Role');
if ($result === SAVED_NEW) {
// Grant the specified permissions to the role, if any.
if (!empty($permissions)) {
user_role_grant_permissions($role->id(), $permissions);
$assigned_permissions = Role::load($role->id())->getPermissions();
$missing_permissions = array_diff($permissions, $assigned_permissions);
if (!$missing_permissions) {
$this->pass(SafeMarkup::format('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), 'Role');
}
else {
$this->fail(SafeMarkup::format('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions))), 'Role');
}
}
return $role->id();
}
else {
return FALSE;
}
}
/**
* Checks whether a given list of permission names is valid.
*
* @param array $permissions
* The permission names to check.
*
* @return bool
* TRUE if the permissions are valid, FALSE otherwise.
*/
protected function checkPermissions(array $permissions) {
$available = array_keys(\Drupal::service('user.permissions')->getPermissions());
$valid = TRUE;
foreach ($permissions as $permission) {
if (!in_array($permission, $available)) {
$this->fail(SafeMarkup::format('Invalid permission %permission.', array('%permission' => $permission)), 'Role');
$valid = FALSE;
}
}
return $valid;
}
/**
* Log in a user with the internal browser.
*
......
......@@ -12,11 +12,11 @@
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait;
use Drupal\views\Plugin\views\style\Table;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Defines a actions-based bulk operation form element.
......@@ -26,6 +26,7 @@
class BulkForm extends FieldPluginBase {
use RedirectDestinationTrait;
use UncacheableFieldHandlerTrait;
/**
* The action storage.
......@@ -134,13 +135,6 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
$form_state->setValue(array('options', 'selected_actions'), array_values(array_filter($selected_actions)));
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
return '<!--form-item-' . $this->options['id'] . '--' . $values->index . '-->';
}
/**
* {@inheritdoc}
*/
......@@ -157,6 +151,12 @@ public function preRender(&$values) {
}
}
/**
* {@inheritdoc}
*/
public function getValue(ResultRow $row, $field = NULL) {
return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
}
/**
* Form constructor for the bulk form.
......
......@@ -48,13 +48,14 @@ public function testEntityOperationAlter() {