Commit 3a7965ff authored by Ken Rickard's avatar Ken Rickard
Browse files

Issue #3201775 by agentrickard, joshua1234511, ramya.shankaralingam: The views...

Issue #3201775 by agentrickard, joshua1234511, ramya.shankaralingam: The views filter needs to have cacheability metadata applied when using Views caching
parent 5d681486
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -338,4 +338,22 @@ class Section extends ManyToOne {
    return $children;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    $contexts = parent::getCacheContexts();
    $contexts[] = 'user';
    return $contexts;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $tags = parent::getCacheTags();
    $tags[] = 'workbench_access_view';
    return $tags;
  }

}
+18 −0
Original line number Diff line number Diff line
@@ -143,4 +143,22 @@ class SectionId extends ManyToOne {
    return $operators;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    $contexts = parent::getCacheContexts();
    $contexts[] = 'user';
    return $contexts;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $tags = parent::getCacheTags();
    $tags[] = 'workbench_access_view';
    return $tags;
  }

}
+5 −1
Original line number Diff line number Diff line
@@ -215,7 +215,11 @@ class UserSectionStorage implements UserSectionStorageInterface {
    }
    // Invalidate entity access tags that we use.
    // @TODO: we should inject the cache service.
    Cache::invalidateTags(['config:workbench_access.access_scheme.' . $scheme->id()]);
    Cache::invalidateTags([
      'config:workbench_access.access_scheme.' . $scheme->id(),
      'workbench_access_view'
]
    );
  }

  /**
+251 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\workbench_access\Functional;

use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\workbench_access\Entity\AccessScheme;
use Drupal\views\Views;

/**
 * Defines a class for testing workbench access views.
 *
 * @group workbench_access
 */
class ViewsCacheTest extends BrowserTestBase {

  use AssertPageCacheContextsAndTagsTrait;

  /**
   * The default theme.
   *
   * @var string
   */
  protected $defaultTheme = 'stable';

  /**
   * Test terms.
   *
   * @var \Drupal\taxonomy\TermInterface[]
   */
  protected $terms = [];

  /**
   * Test nodes.
   *
   * @var \Drupal\node\NodeInterface[]
   */
  protected $nodes = [];

  /**
   * Test user.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $user;

  /**
   * Test user.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $user2;

  /**
   * User section storage.
   *
   * @var \Drupal\workbench_access\UserSectionStorage
   */
  protected $userStorage;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'workbench_access',
    'views',
    'node',
    'taxonomy',
    'system',
    'user',
    'filter',
    'workbench_access_test',
  ];

  /**
   * Access scheme.
   *
   * @var \Drupal\workbench_access\Entity\AccessSchemeInterface
   */
  protected $scheme;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    // Create some sections and some nodes in them.
    $sections = [
      'Some section',
      'Another section',
      'More sections',
    ];
    foreach ($sections as $section) {
      $this->terms[$section] = Term::create([
        'vid' => 'editorial_section',
        'name' => $section . ' term',
      ]);
      $this->terms[$section]->save();
      foreach ([' node 1', ' node 2'] as $stub) {
        $title = $section . $stub;
        $this->nodes[$title] = Node::create([
          'type' => 'article',
          'title' => $title,
          'status' => 1,
          'field_workbench_access' => $this->terms[$section],
        ]);
        $this->nodes[$title]->save();
      }
    }

    // Create a user who can access content etc.
    $permissions = [
      'create article content',
      'edit any article content',
      'access content',
      'delete any article content',
      'administer nodes',
      'access user profiles',
      'use workbench access',
    ];
    $this->user = $this->createUser($permissions);
    $this->user->save();
    $this->userStorage = \Drupal::service('workbench_access.user_section_storage');
    $this->scheme = AccessScheme::load('editorial_section');

    $values = array_values(array_map(function (TermInterface $term) {
      return $term->id();
    }, $this->terms));
    $this->userStorage->addUser($this->scheme, $this->user, $values);

    $this->user2 = $this->createUser($permissions);
    $this->user2->save();
    $values = [reset($this->terms)->id()];
    $this->userStorage->addUser($this->scheme, $this->user2, $values);
  }

  /**
   * Tests that views cache as expected.
   *
   * See \Drupal\Tests\views\Functional\Plugin\CacheWebTest for a similar test.
   */
  public function testViewsCache() {
    // A view that does not contain a workbench_access element.
    $view = Views::getView('user_admin_people');
    $context = [
      'languages:language_content',
      'languages:language_interface',
      'url',
      'url.query_args',
      'user.permissions',
    ];
    $no_workbench_access = $view->render('page_1');
    $this->assertEquals($context, $no_workbench_access['#cache']['contexts']);

    // A view that does.
    $view2 = Views::getView('content_sections');
    // The context from the View.
    // tests/modules/workbench_access_test/config/install/views.view.content_sections.yml.
    $context2 = [
      'languages:language_content',
      'languages:language_interface',
      'url.query_args',
      'user.node_grants:view',
      'user.permissions',
    ];
    // What we add via hook_views_post_render().
    $context2[] = 'user';

    $workbench_access = $view2->render('page_1');
    $this->assertEquals($context2, $workbench_access['#cache']['contexts']);

    // Now test as user 1 who has access to all sections.
    $this->drupalLogin($this->user);
    $this->drupalGet('admin/content/sections');

    $allowed = [];
    $disallowed = [];
    foreach ($this->terms as $section => $term) {
      $allowed[] = $section;
    }
    $this->assertSectionVisibility($allowed, $disallowed);

    // Now test as user 2 who only has access to the first section.
    $this->drupalLogin($this->user2);
    $this->drupalGet('admin/content/sections');
    $allowed_1 = [
      'Some section',
    ];
    $disallowed_1 = [
      'Another section',
      'More sections',
    ];
    $this->assertSectionVisibility($allowed_1, $disallowed_1);

    // Add user 2 to another section and re-test.
    $term = $this->terms['Another section']->id();
    $this->userStorage->addUser($this->scheme, $this->user2, [$term]);
    $this->drupalGet('admin/content/sections');
    $allowed_2 = [
      'Some section',
      'Another section',
    ];
    $disallowed_2 = [
      'More sections',
    ];
    $this->assertSectionVisibility($allowed_2, $disallowed_2);

    // Remove the user and re-test.
    $this->userStorage->removeUser($this->scheme, $this->user2, [$term]);
    $this->drupalGet('admin/content/sections');
    $this->assertSectionVisibility($allowed_1, $disallowed_1);

    // Add the user to a role and re-test.
    $roles = $this->user2->getRoles(TRUE);
    $role_storage = \Drupal::service('workbench_access.role_section_storage');
    $role_storage->addRole($this->scheme, $roles[0], [$term]);
    $this->drupalGet('admin/content/sections');
    $this->assertSectionVisibility($allowed_2, $disallowed_2);

    // Remove and re-test.
    $role_storage->removeRole($this->scheme, $roles[0], [$term]);
    $this->drupalGet('admin/content/sections');
    $this->assertSectionVisibility($allowed_1, $disallowed_1);
  }

  /**
   * Asserts what can and cannot be seen by the user.
   *
   * @param array $allowed
   *   The items we should see.
   * @param array $disallowed
   *   The items we should not see.
   */
  public function assertSectionVisibility(array $allowed, array $disallowed) {
    $assert = $this->assertSession();
    foreach ($allowed as $item) {
      $assert->elementExists('css', '.views-row:contains("' . $item . ' node 1' . '")');
      $assert->elementExists('css', '.views-row:contains("' . $item . ' node 2' . '")');
    }
    foreach ($disallowed as $item) {
      $assert->elementNotExists('css', '.views-row:contains("' . $item . ' node 1' . '")');
      $assert->elementNotExists('css', '.views-row:contains("' . $item . ' node 2' . '")');
    }
    // Check the cache tags.
    $assert->responseHeaderContains('X-Drupal-Cache-Tags', 'workbench_access_view');
  }

}
+47 −1
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@ use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\workbench_access\Entity\AccessSchemeInterface;
use Drupal\workbench_access\FormAlterHelper;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\ViewExecutable;

/**
 * Implements hook_help().
@@ -132,7 +134,7 @@ function workbench_access_views_data_alter(array &$data) {
  $scheme_storage = \Drupal::entityTypeManager()->getStorage('access_scheme');
  if ($schemes = $scheme_storage->loadMultiple()) {
    /** @var \Drupal\workbench_access\Entity\AccessSchemeInterface $scheme */
    foreach ($schemes as $id => $scheme) {
    foreach ($schemes as $scheme) {
      $scheme->getAccessScheme()->viewsData($data, $scheme);
      $data['users']['workbench_access_section__' . $scheme->id()] = [
        'title' => t('Workbench Section: @name', [
@@ -246,3 +248,47 @@ function workbench_access_token_info() {
function workbench_access_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  return \Drupal::service('workbench_access.tokens')->getTokens($type, $tokens, $data, $options, $bubbleable_metadata);
}

/**
 * Determines if a view uses Workbench Access.
 *
 * We use this function to determine if the view needs cache tags.
 *
 * @param \Drupal\views\ViewExecutable $view
 *   The view being rendered.
 *
 * @return bool
 *   Whether to add cache tags or not.
 */
function workbench_access_view_check(ViewExecutable $view) {
  static $is_workbench_access_view = [];
  $id = $view->id();
  if (!isset($is_workbench_access_view[$id])) {
    $is_workbench_access_view[$id] = FALSE;
    $handler_types = $view->getHandlerTypes();
    $plugins = ['workbench_access_section', 'workbench_access_user_section'];
    foreach ($handler_types as $type => $info) {
      foreach ($view->getHandlers($type) as $items) {
        if (in_array($items['plugin_id'], $plugins, TRUE)) {
          $is_workbench_access_view[$id] = TRUE;
        }
      }
    }
  }

  return $is_workbench_access_view[$id];
}

/**
 * Implements hook_views_post_render().
 *
 * Cache workbench_access views per-user automatically.
 */
function workbench_access_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
  // Add user cache tags to views if required.
  if (workbench_access_view_check($view)) {
    // Add the user cache context.
    $view->addCacheContext('user');
    $output['#cache']['tags'][] = 'workbench_access_view';
  }
}