diff --git a/src/TwigExtension.php b/src/TwigExtension.php index 71bbaa8..244f48f 100644 --- a/src/TwigExtension.php +++ b/src/TwigExtension.php @@ -2,7 +2,9 @@ namespace Drupal\twig_tweak; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\TitleBlockPluginInterface; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Url; @@ -80,8 +82,16 @@ class TwigExtension extends \Twig_Extension { public function drupalBlock($id, $check_access = TRUE) { $entity_type_manager = \Drupal::entityTypeManager(); $block = $entity_type_manager->getStorage('block')->load($id); - if ($block && (!$check_access || $this->entityAccess($block))) { - return $entity_type_manager->getViewBuilder('block')->view($block); + if ($block) { + $access = $check_access ? $this->entityAccess($block) : AccessResult::allowed(); + if ($access->isAllowed()) { + $build = $entity_type_manager->getViewBuilder('block')->view($block); + CacheableMetadata::createFromRenderArray($build) + ->merge(CacheableMetadata::createFromObject($block)) + ->merge(CacheableMetadata::createFromObject($access)) + ->applyTo($build); + return $build; + } } } @@ -108,9 +118,13 @@ class TwigExtension extends \Twig_Extension { $build = []; + $cache_metadata = new CacheableMetadata(); + /* @var $blocks \Drupal\block\BlockInterface[] */ foreach ($blocks as $id => $block) { - if ($this->entityAccess($block)) { + $access = $this->entityAccess($block); + $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($access)); + if ($access->isAllowed()) { $block_plugin = $block->getPlugin(); if ($block_plugin instanceof TitleBlockPluginInterface) { $request = \Drupal::request(); @@ -122,6 +136,10 @@ class TwigExtension extends \Twig_Extension { } } + if ($build) { + $cache_metadata->applyTo($build); + } + return $build; } @@ -146,9 +164,18 @@ class TwigExtension extends \Twig_Extension { $entity = $id ? $entity_type_manager->getStorage($entity_type)->load($id) : \Drupal::routeMatch()->getParameter($entity_type); - if ($entity && $this->entityAccess($entity)) { - $render_controller = $entity_type_manager->getViewBuilder($entity_type); - return $render_controller->view($entity, $view_mode, $langcode); + if ($entity) { + $access = $this->entityAccess($entity); + if ($access->isAllowed()) { + $build = $entity_type_manager + ->getViewBuilder($entity_type) + ->view($entity, $view_mode, $langcode); + CacheableMetadata::createFromRenderArray($build) + ->merge(CacheableMetadata::createFromObject($entity)) + ->merge(CacheableMetadata::createFromObject($access)) + ->applyTo($build); + return $build; + } } } @@ -174,12 +201,20 @@ class TwigExtension extends \Twig_Extension { $entity = $id ? \Drupal::entityTypeManager()->getStorage($entity_type)->load($id) : \Drupal::routeMatch()->getParameter($entity_type); - if ($entity && $this->entityAccess($entity)) { - if ($langcode && $entity->hasTranslation($langcode)) { - $entity = $entity->getTranslation($langcode); - } - if (isset($entity->{$field_name})) { - return $entity->{$field_name}->view($view_mode); + if ($entity) { + $access = $this->entityAccess($entity); + if ($access->isAllowed()) { + if ($langcode && $entity->hasTranslation($langcode)) { + $entity = $entity->getTranslation($langcode); + } + if (isset($entity->{$field_name})) { + $build = $entity->{$field_name}->view($view_mode); + CacheableMetadata::createFromRenderArray($build) + ->merge(CacheableMetadata::createFromObject($access)) + ->merge(CacheableMetadata::createFromObject($entity)) + ->applyTo($build); + return $build; + } } } } @@ -481,7 +516,7 @@ class TwigExtension extends \Twig_Extension { * @param \Drupal\Core\Entity\EntityInterface $entity * Entity to check access. * - * @return bool + * @return \Drupal\Core\Access\AccessResultInterface * The access check result. * * @TODO Remove "check_access" option in 9.x. @@ -489,7 +524,8 @@ class TwigExtension extends \Twig_Extension { protected function entityAccess(EntityInterface $entity) { // Prior version 8.x-1.7 entity access was not checked. The "check_access" // option provides a workaround for possible BC issues. - return !Settings::get('twig_tweak_check_access', TRUE) || $entity->access('view'); + return Settings::get('twig_tweak_check_access', TRUE) ? + $entity->access('view', NULL, TRUE) : AccessResult::allowed(); } } diff --git a/tests/src/Kernel/AccessTest.php b/tests/src/Kernel/AccessTest.php new file mode 100644 index 0000000..623114e --- /dev/null +++ b/tests/src/Kernel/AccessTest.php @@ -0,0 +1,212 @@ +installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['system']); + + $node_type = NodeType::create([ + 'type' => 'article', + 'name' => 'Article', + ]); + $node_type->save(); + + $values = [ + 'type' => 'article', + 'status' => NodeInterface::PUBLISHED, + // @see twig_tweak_test_node_access() + 'title' => 'Entity access test', + ]; + $this->node = Node::create($values); + $this->node->save(); + + $this->twigExtension = $this->container->get('twig_tweak.twig_extension'); + } + + /** + * Test callback. + */ + public function testDrupalEntity() { + + // -- Unprivileged user. + $this->setUpCurrentUser(['name' => 'User 1']); + + $build = $this->twigExtension->drupalEntity('node', $this->node->id()); + self::assertNull($build); + + // -- Privileged user. + $this->setUpCurrentUser(['name' => 'User 2'], ['access content']); + + $build = $this->twigExtension->drupalEntity('node', $this->node->id()); + self::assertArrayHasKey('#node', $build); + $expected_cache = [ + 'tags' => [ + 'node:1', + 'node_view', + 'tag_from_twig_tweak_test_node_access', + ], + 'contexts' => [ + 'user', + 'user.permissions', + ], + 'max-age' => 50, + ]; + self::assertSame($expected_cache, $build['#cache']); + } + + /** + * Test callback. + */ + public function testDrupalField() { + + // -- Unprivileged user. + $this->setUpCurrentUser(['name' => 'User 1']); + + $build = $this->twigExtension->drupalField('title', 'node', $this->node->id()); + self::assertNull($build); + + // -- Privileged user. + $this->setUpCurrentUser(['name' => 'User 2'], ['access content']); + + $build = $this->twigExtension->drupalField('title', 'node', $this->node->id()); + self::assertArrayHasKey('#items', $build); + $expected_cache = [ + 'contexts' => [ + 'user', + 'user.permissions', + ], + 'tags' => [ + 'node:1', + 'tag_from_twig_tweak_test_node_access', + ], + 'max-age' => 50, + ]; + self::assertSame($expected_cache, $build['#cache']); + } + + /** + * Test callback. + */ + public function testDrupalRegion() { + + // @codingStandardsIgnoreStart + $create_block = function ($id) { + return new class(['id' => $id], 'block') extends Block { + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIf($this->id == 'block_1'); + $result->cachePerUser(); + $result->addCacheTags(['tag_for_' . $this->id]); + $result->setCacheMaxAge(123); + return $return_as_object ? $result : $result->isAllowed(); + } + public function getPlugin() { + return NULL; + } + }; + }; + // @codingStandardsIgnoreEnd + + $storage = $this->createMock(EntityStorageInterface::class); + $blocks = [ + 'block_1' => $create_block('block_1'), + 'block_2' => $create_block('block_2'), + ]; + $storage->expects($this->any()) + ->method('loadByProperties') + ->willReturn($blocks); + + $view_builder = $this->createMock(BlockViewBuilder::class); + $content = [ + '#markup' => 'foo', + '#cache' => [ + 'tags' => ['tag_from_view'], + ], + ]; + $view_builder->expects($this->any()) + ->method('view') + ->willReturn($content); + + $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class); + $entity_type_manager->expects($this->any()) + ->method('getStorage') + ->willReturn($storage); + $entity_type_manager->expects($this->any()) + ->method('getViewBuilder') + ->willReturn($view_builder); + + $this->container->set('entity_type.manager', $entity_type_manager); + + $build = $this->twigExtension->drupalRegion('bar'); + $expected_build = [ + 'block_1' => [ + '#markup' => 'foo', + '#cache' => [ + 'tags' => ['tag_from_view'], + ], + ], + '#cache' => [ + 'contexts' => ['user'], + 'tags' => [ + 'tag_for_block_1', + 'tag_for_block_2', + ], + 'max-age' => 123, + ], + ]; + self::assertSame($expected_build, $build); + } + +} diff --git a/tests/twig_tweak_test/twig_tweak_test.module b/tests/twig_tweak_test/twig_tweak_test.module index f7d00aa..2d0c0fa 100644 --- a/tests/twig_tweak_test/twig_tweak_test.module +++ b/tests/twig_tweak_test/twig_tweak_test.module @@ -5,6 +5,9 @@ * Primary module hooks for Twig Tweak test module. */ +use Drupal\Core\Access\AccessResult; +use Drupal\node\NodeInterface; + /** * Implements hook_page_bottom(). */ @@ -25,3 +28,18 @@ function twig_tweak_test_theme() { function template_preprocess_twig_tweak_test(&$vars) { $vars['node'] = Drupal::routeMatch()->getParameter('node'); } + +/** + * Implements hook_node_access(). + * + * @see \Drupal\Tests\twig_tweak\Kernel\AccessTest + */ +function twig_tweak_test_node_access(NodeInterface $node) { + if ($node->getTitle() == 'Entity access test') { + $result = AccessResult::allowed(); + $result->addCacheTags(['tag_from_' . __FUNCTION__]); + $result->cachePerUser(); + $result->setCacheMaxAge(50); + return $result; + } +}