Issue #3007424 by acbramley, Spokje, mbovan, narendra.rajwar27, AaronBauman, ravi.shankar, aleevas, geek-merlin, jibran, YurkinPark, daffie, larowlan, alexpott, marcelovani, catch, Berdir, Lendude, mstrelan, tar_inet, andrewbelcher, seanB, Honza Pobořil, danflanagan8, susgo, yogeshmpawar, sylus, sjpeters79, raman.b: Multiple usages of FieldPluginBase::getEntity do not check for NULL, leading to WSOD

merge-requests/3778/head
Lee Rowlands 2023-04-04 14:37:06 +10:00
parent 5375e507d8
commit 7ea52649ec
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
30 changed files with 856 additions and 48 deletions

View File

@ -76,7 +76,10 @@ class EntityLink extends FieldPluginBase {
$entity = $this->getEntity($values);
// Only render the links, if they are defined.
return !empty($this->build[$entity->id()]['links']['comment__comment']) ? \Drupal::service('renderer')->render($this->build[$entity->id()]['links']['comment__comment']) : '';
if (!$entity || empty($this->build[$entity->id()]['links']['comment__comment'])) {
return '';
}
return \Drupal::service('renderer')->render($this->build[$entity->id()]['links']['comment__comment']);
}
}

View File

@ -19,7 +19,11 @@ class LinkApprove extends LinkBase {
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
return Url::fromRoute('comment.approve', ['comment' => $this->getEntity($row)->id()]);
$entity = $this->getEntity($row);
if (!$entity) {
return NULL;
}
return Url::fromRoute('comment.approve', ['comment' => $entity->id()]);
}
/**

View File

@ -21,6 +21,9 @@ class LinkReply extends LinkBase {
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $this->getEntity($row);
if (!$comment) {
return NULL;
}
return Url::fromRoute('comment.reply', [
'entity_type' => $comment->getCommentedEntityTypeId(),
'entity' => $comment->getCommentedEntityId(),

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\Tests\comment\Unit\Plugin\views\field;
use Drupal\comment\Plugin\views\field\EntityLink;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\comment\Plugin\views\field\EntityLink
* @group comment
*/
class EntityLinkTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new EntityLink(['entity_type' => 'foo', 'entity field' => 'bar'], '', []);
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\comment\Unit\Plugin\views\field;
use Drupal\comment\Plugin\views\field\LinkApprove;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\comment\Plugin\views\field\LinkApprove
* @group comment
*/
class LinkApproveTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new LinkApprove(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(AccessManagerInterface::class), $this->createMock(EntityTypeManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(LanguageManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\comment\Unit\Plugin\views\field;
use Drupal\comment\Plugin\views\field\LinkReply;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\comment\Plugin\views\field\LinkReply
* @group comment
*/
class LinkReplyTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new LinkReply(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(AccessManagerInterface::class), $this->createMock(EntityTypeManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(LanguageManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -30,7 +30,11 @@ class ContactLink extends LinkBase {
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
return Url::fromRoute('entity.user.contact_form', ['user' => $this->getEntity($row)->id()]);
$entity = $this->getEntity($row);
if (!$entity) {
return NULL;
}
return Url::fromRoute('entity.user.contact_form', ['user' => $entity->id()]);
}
/**
@ -38,6 +42,9 @@ class ContactLink extends LinkBase {
*/
protected function renderLink(ResultRow $row) {
$entity = $this->getEntity($row);
if (!$entity) {
return '';
}
$this->options['alter']['make_link'] = TRUE;
$this->options['alter']['url'] = $this->getUrlInfo($row);

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\contact\Unit;
use Drupal\contact\Plugin\views\field\ContactLink;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\contact\Plugin\views\field\ContactLink
* @group contact
*/
class ContactLinkTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new ContactLink(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(AccessManagerInterface::class), $this->createMock(EntityTypeManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(LanguageManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -277,9 +277,7 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
foreach (Element::getVisibleChildren($form['media_bulk_form']) as $key) {
if (isset($view->result[$key])) {
$media = $view->field['media_bulk_form']->getEntity($view->result[$key]);
$form['media_bulk_form'][$key]['#title'] = t('Select @label', [
'@label' => $media->label(),
]);
$form['media_bulk_form'][$key]['#title'] = $media ? t('Select @label', ['@label' => $media->label()]) : '';
}
}
}

View File

@ -56,6 +56,10 @@ class MediaLibrarySelectForm extends FieldPluginBase {
$form[$this->options['id']]['#tree'] = TRUE;
foreach ($this->view->result as $row_index => $row) {
$entity = $this->getEntity($row);
if (!$entity) {
$form[$this->options['id']][$row_index] = [];
continue;
}
$form[$this->options['id']][$row_index] = [
'#type' => 'checkbox',
'#title' => $this->t('Select @label', [

View File

@ -0,0 +1,115 @@
<?php
namespace Drupal\Tests\media_library\Unit;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\media_library\Plugin\views\field\MediaLibrarySelectForm;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Entity\View;
use Drupal\views\Plugin\views\display\DefaultDisplay;
use Drupal\views\Plugin\ViewsPluginManager;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\media_library\Plugin\views\field\MediaLibrarySelectForm
* @group media_library
*/
class MediaLibrarySelectFormTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
parent::tearDown();
$container = new ContainerBuilder();
\Drupal::setContainer($container);
}
/**
* @covers ::viewsForm
*/
public function testViewsForm(): void {
$row = new ResultRow();
$field = $this->getMockBuilder(MediaLibrarySelectForm::class)
->onlyMethods(['getEntity'])
->disableOriginalConstructor()
->getMock();
$field->expects($this->any())
->method('getEntity')
->willReturn(NULL);
$container = new ContainerBuilder();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
$query = $this->getMockBuilder(ParameterBag::class)
->onlyMethods(['all'])
->disableOriginalConstructor()
->getMock();
$query->expects($this->any())
->method('all')
->willReturn([]);
$request = $this->getMockBuilder(Request::class)
->disableOriginalConstructor()
->getMock();
$request->query = $query;
$view = $this->getMockBuilder(ViewExecutable::class)
->onlyMethods(['getRequest', 'initStyle', 'getDisplay'])
->disableOriginalConstructor()
->getMock();
$view->expects($this->any())
->method('getRequest')
->willReturn($request);
$view->expects($this->any())
->method('initStyle')
->willReturn(TRUE);
$display = $this->getMockBuilder(DefaultDisplay::class)
->disableOriginalConstructor()
->getMock();
$display->display['id'] = 'foo';
$view->expects($this->any())
->method('getDisplay')
->willReturn($display);
$view_entity = $this->getMockBuilder(View::class)
->disableOriginalConstructor()
->getMock();
$view_entity->expects($this->any())
->method('get')
->willReturn([]);
$view->storage = $view_entity;
$display_manager = $this->getMockBuilder(ViewsPluginManager::class)
->disableOriginalConstructor()
->getMock();
$display = $this->getMockBuilder(DefaultDisplay::class)
->disableOriginalConstructor()
->getMock();
$display_manager->expects($this->any())
->method('createInstance')
->willReturn($display);
$container->set('plugin.manager.views.display', $display_manager);
\Drupal::setContainer($container);
$form_state = $this->createMock(FormStateInterface::class);
$view->result = [$row];
$field->view = $view;
$field->options = ['id' => 'bar'];
$form = [];
$field->viewsForm($form, $form_state);
$this->assertNotEmpty($form);
$this->assertNotEmpty($field->view->result);
$this->assertIsArray($form[$field->options['id']][0]);
$this->assertEmpty($form[$field->options['id']][0]);
}
}

View File

@ -21,9 +21,15 @@ class RevisionLink extends LinkBase {
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
if (!$node) {
return NULL;
}
// Current revision uses the node view path.
return !$node->isDefaultRevision() ?
Url::fromRoute('entity.node.revision', ['node' => $node->id(), 'node_revision' => $node->getRevisionId()]) :
Url::fromRoute('entity.node.revision', [
'node' => $node->id(),
'node_revision' => $node->getRevisionId(),
]) :
$node->toUrl();
}
@ -33,7 +39,7 @@ class RevisionLink extends LinkBase {
protected function renderLink(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
if (!$node->getRevisionid()) {
if (!$node || !$node->getRevisionid()) {
return '';
}
$text = parent::renderLink($row);

View File

@ -20,6 +20,9 @@ class RevisionLinkDelete extends RevisionLink {
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
if (!$node) {
return NULL;
}
return Url::fromRoute('node.revision_delete_confirm', ['node' => $node->id(), 'node_revision' => $node->getRevisionId()]);
}

View File

@ -20,6 +20,9 @@ class RevisionLinkRevert extends RevisionLink {
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
if (!$node) {
return NULL;
}
return Url::fromRoute('node.revision_revert_confirm', ['node' => $node->id(), 'node_revision' => $node->getRevisionId()]);
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\node\Unit\Plugin\views\field;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\node\Plugin\views\field\RevisionLinkDelete;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\node\Plugin\views\field\RevisionLinkDelete
* @group node
*/
class RevisionLinkDeleteTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new RevisionLinkDelete(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(AccessManagerInterface::class), $this->createMock(EntityTypeManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(LanguageManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\node\Unit\Plugin\views\field;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\node\Plugin\views\field\RevisionLinkDelete;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\node\Plugin\views\field\RevisionLinkRevert
* @group node
*/
class RevisionLinkRevertTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new RevisionLinkDelete(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(AccessManagerInterface::class), $this->createMock(EntityTypeManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(LanguageManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\node\Unit\Plugin\views\field;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\node\Plugin\views\field\RevisionLink;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\node\Plugin\views\field\RevisionLink
* @group node
*/
class RevisionLinkTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
}
/**
* Test the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new RevisionLink(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(AccessManagerInterface::class), $this->createMock(EntityTypeManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(LanguageManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -87,11 +87,14 @@ class Permissions extends PrerenderList {
$rids = [];
foreach ($values as $result) {
$user_rids = $this->getEntity($result)->getRoles();
$uid = $this->getValue($result);
$user = $this->getEntity($result);
if ($user) {
$user_rids = $user->getRoles();
$uid = $this->getValue($result);
foreach ($user_rids as $rid) {
$rids[$rid][] = $uid;
foreach ($user_rids as $rid) {
$rids[$rid][] = $uid;
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Drupal\Tests\user\Unit\Plugin\views\field;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\Plugin\views\field\Permissions;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\user\Plugin\views\field\Permissions
* @group user
*/
class PermissionsTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
$container->set('user.permissions', $this->createMock(PermissionHandlerInterface::class));
\Drupal::setContainer($container);
}
/**
* Tests the preRender method when getEntity returns NULL.
*
* @covers ::preRender
*/
public function testPreRenderNullEntity(): void {
$values = [new ResultRow()];
$field = new Permissions(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(ModuleHandlerInterface::class), $this->createMock(EntityTypeManagerInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$field->preRender($values);
$this->assertEmpty($field->items);
}
}

View File

@ -289,17 +289,24 @@ class BulkForm extends FieldPluginBase implements CacheableDependencyInterface {
// Render checkboxes for all rows.
$form[$this->options['id']]['#tree'] = TRUE;
foreach ($this->view->result as $row_index => $row) {
$entity = $this->getEntityTranslationByRelationship($this->getEntity($row), $row);
$entity = $this->getEntity($row);
if ($entity !== NULL) {
$entity = $this->getEntityTranslationByRelationship($entity, $row);
$form[$this->options['id']][$row_index] = [
'#type' => 'checkbox',
// We are not able to determine a main "title" for each row, so we
// can only output a generic label.
'#title' => $this->t('Update this item'),
'#title_display' => 'invisible',
'#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL,
'#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision),
];
}
else {
$form[$this->options['id']][$row_index] = [];
}
$form[$this->options['id']][$row_index] = [
'#type' => 'checkbox',
// We are not able to determine a main "title" for each row, so we can
// only output a generic label.
'#title' => $this->t('Update this item'),
'#title_display' => 'invisible',
'#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL,
'#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision),
];
}
// Replace the form submit button label.

View File

@ -26,7 +26,8 @@ class EntityLink extends LinkBase {
*/
protected function renderLink(ResultRow $row) {
if ($this->options['output_url_as_text']) {
return $this->getUrlInfo($row)->toString();
$url_info = $this->getUrlInfo($row);
return $url_info ? $url_info->toString() : '';
}
return parent::renderLink($row);
}
@ -37,6 +38,9 @@ class EntityLink extends LinkBase {
protected function getUrlInfo(ResultRow $row) {
$template = $this->getEntityLinkTemplate();
$entity = $this->getEntity($row);
if ($entity === NULL) {
return NULL;
}
if ($this->languageManager->isMultilingual()) {
$entity = $this->getEntityTranslationByRelationship($entity, $row);
}

View File

@ -420,12 +420,27 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf
*/
public function getEntity(ResultRow $values) {
$relationship_id = $this->options['relationship'];
$entity = NULL;
if ($relationship_id == 'none') {
return $values->_entity;
$entity = $values->_entity;
}
elseif (isset($values->_relationship_entities[$relationship_id])) {
return $values->_relationship_entities[$relationship_id];
$entity = $values->_relationship_entities[$relationship_id];
}
if ($entity === NULL) {
\Drupal::logger('views')->error(
'The view %id failed to load an entity of type %entity_type at row %index for field %field',
[
'%id' => $this->view->id(),
'%entity_type' => $this->configuration['entity_type'],
'%index' => $values->index,
'%field' => $this->configuration['entity field'],
]
);
return NULL;
}
return $entity;
}
/**

View File

@ -164,9 +164,12 @@ abstract class LinkBase extends FieldPluginBase {
*/
public function render(ResultRow $row) {
$access = $this->checkUrlAccess($row);
$build = ['#markup' => $access->isAllowed() ? $this->renderLink($row) : ''];
BubbleableMetadata::createFromObject($access)->applyTo($build);
return $build;
if ($access) {
$build = ['#markup' => $access->isAllowed() ? $this->renderLink($row) : ''];
BubbleableMetadata::createFromObject($access)->applyTo($build);
return $build;
}
return '';
}
/**
@ -175,12 +178,13 @@ abstract class LinkBase extends FieldPluginBase {
* @param \Drupal\views\ResultRow $row
* A view result row.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
* @return \Drupal\Core\Access\AccessResultInterface|null
* The access result, or NULL if the URI elements of the link doesn't exist.
*/
protected function checkUrlAccess(ResultRow $row) {
$url = $this->getUrlInfo($row);
return $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $this->currentUser(), TRUE);
if ($url = $this->getUrlInfo($row)) {
return $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $this->currentUser(), TRUE);
}
}
/**
@ -189,7 +193,7 @@ abstract class LinkBase extends FieldPluginBase {
* @param \Drupal\views\ResultRow $row
* A view result row.
*
* @return \Drupal\Core\Url
* @return \Drupal\Core\Url|null
* The URI elements of the link.
*/
abstract protected function getUrlInfo(ResultRow $row);
@ -219,7 +223,7 @@ abstract class LinkBase extends FieldPluginBase {
*/
protected function addLangcode(ResultRow $row) {
$entity = $this->getEntity($row);
if ($this->languageManager->isMultilingual()) {
if ($entity && $this->languageManager->isMultilingual()) {
$this->options['alter']['language'] = $this->getEntityTranslationByRelationship($entity, $row)->language();
}
}

View File

@ -129,15 +129,17 @@ class RenderedEntity extends FieldPluginBase implements CacheableDependencyInter
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$entity = $this->getEntityTranslationByRelationship($this->getEntity($values), $values);
$entity = $this->getEntity($values);
if ($entity === NULL) {
return '';
}
$entity = $this->getEntityTranslationByRelationship($entity, $values);
$build = [];
if (isset($entity)) {
$access = $entity->access('view', NULL, TRUE);
$build['#access'] = $access;
if ($access->isAllowed()) {
$view_builder = $this->entityTypeManager->getViewBuilder($this->getEntityTypeId());
$build += $view_builder->view($entity, $this->options['view_mode'], $entity->language()->getId());
}
$access = $entity->access('view', NULL, TRUE);
$build['#access'] = $access;
if ($access->isAllowed()) {
$view_builder = $this->entityTypeManager->getViewBuilder($this->getEntityTypeId());
$build += $view_builder->view($entity, $this->options['view_mode'], $entity->language()->getId());
}
return $build;
}

View File

@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\views\Traits;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Provides helper functions for logging in views.
*/
trait ViewsLoggerTestTrait {
/**
* Sets up a mock logger for when views can't load an entity.
*/
public function setUpMockLoggerWithMissingEntity(): void {
$loggerFactory = $this->createMock(LoggerChannelFactoryInterface::class);
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())
->method('error')
->with(
'The view %id failed to load an entity of type %entity_type at row %index for field %field',
$this->anything(),
);
$loggerFactory->expects($this->once())
->method('get')
->willReturn($logger);
$container = new ContainerBuilder();
$container->set('logger.factory', $loggerFactory);
\Drupal::setContainer($container);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\views\Unit\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Plugin\views\field\BulkForm;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @coversDefaultClass \Drupal\views\Plugin\views\field\BulkForm
* @group Views
*/
class BulkFormTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
parent::tearDown();
$container = new ContainerBuilder();
\Drupal::setContainer($container);
}
/**
* @covers ::viewsForm
*/
public function testViewsForm(): void {
$row = new ResultRow();
$container = new ContainerBuilder();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
$field = $this->getMockBuilder(BulkForm::class)
->onlyMethods(['getEntityType', 'getEntity'])
->disableOriginalConstructor()
->getMock();
$field->expects($this->any())
->method('getEntityType')
->willReturn('foo');
$field->expects($this->any())
->method('getEntity')
->willReturn(NULL);
$query = $this->getMockBuilder(QueryPluginBase::class)
->onlyMethods(['getEntityTableInfo'])
->disableOriginalConstructor()
->getMock();
$query->expects($this->any())
->method('getEntityTableInfo')
->willReturn([]);
$view = $this->getMockBuilder(ViewExecutable::class)
->onlyMethods(['getQuery'])
->disableOriginalConstructor()
->getMock();
$view->expects($this->any())
->method('getQuery')
->willReturn($query);
$view->result = [$row];
$view->query = $query;
$field->view = $view;
$field->options = ['id' => 'bar', 'action_title' => 'zee'];
$form_state = $this->createMock(FormStateInterface::class);
$form = [];
$field->viewsForm($form, $form_state);
$this->assertNotEmpty($form);
$this->assertIsArray($form[$field->options['id']][0]);
$this->assertEmpty($form[$field->options['id']][0]);
}
}

View File

@ -5,6 +5,7 @@ namespace Drupal\Tests\views\Unit\Plugin\views\field;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\field\EntityOperations;
use Drupal\views\ResultRow;
@ -14,6 +15,8 @@ use Drupal\views\ResultRow;
*/
class EntityOperationsUnitTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* The entity type manager.
*
@ -52,7 +55,7 @@ class EntityOperationsUnitTest extends UnitTestCase {
$this->entityRepository = $this->createMock(EntityRepositoryInterface::class);
$this->languageManager = $this->createMock('\Drupal\Core\Language\LanguageManagerInterface');
$configuration = [];
$configuration = ['entity_type' => 'foo', 'entity field' => 'bar'];
$plugin_id = $this->randomMachineName();
$plugin_definition = [
'title' => $this->randomMachineName(),
@ -178,6 +181,8 @@ class EntityOperationsUnitTest extends UnitTestCase {
* @covers ::render
*/
public function testRenderWithoutEntity() {
$this->setUpMockLoggerWithMissingEntity();
$entity = NULL;
$result = new ResultRow();

View File

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\views\Unit\Plugin\views\field;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\LinkBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\views\Plugin\views\field\EntityLink
* @group Views
*/
class LinkBaseTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
$container = \Drupal::getContainer();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
$container->set('renderer', $this->createMock(RendererInterface::class));
\Drupal::setContainer($container);
}
/**
* Tests the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$access = new AccessResultAllowed();
$languageManager = $this->createMock(LanguageManagerInterface::class);
$languageManager->expects($this->any())
->method('isMultilingual')
->willReturn(TRUE);
$field = $this->getMockBuilder(LinkBase::class)
->setConstructorArgs([
['entity_type' => 'foo', 'entity field' => 'bar'],
'foo',
[],
$this->createMock(AccessManagerInterface::class),
$this->createMock(EntityTypeManagerInterface::class),
$this->createMock(EntityRepositoryInterface::class),
$languageManager,
])
->onlyMethods(['checkUrlAccess', 'getUrlInfo'])
->getMock();
$field->expects($this->any())
->method('checkUrlAccess')
->willReturn($access);
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$field_built = $field->render($row);
$this->assertEquals('', \Drupal::service('renderer')->render($field_built));
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Drupal\Tests\views\Unit\Plugin\views\field;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\views\Traits\ViewsLoggerTestTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\RenderedEntity;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* @coversDefaultClass \Drupal\views\Plugin\views\field\RenderedEntity
* @group Views
*/
class RenderedEntityTest extends UnitTestCase {
use ViewsLoggerTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMockLoggerWithMissingEntity();
}
/**
* Tests the render method when getEntity returns NULL.
*
* @covers ::render
*/
public function testRenderNullEntity(): void {
$row = new ResultRow();
$field = new RenderedEntity(['entity_type' => 'foo', 'entity field' => 'bar'], '', [], $this->createMock(EntityTypeManagerInterface::class), $this->createMock(LanguageManagerInterface::class), $this->createMock(EntityRepositoryInterface::class), $this->createMock(EntityDisplayRepositoryInterface::class));
$view = $this->createMock(ViewExecutable::class);
$display = $this->createMock(DisplayPluginBase::class);
$field->init($view, $display);
$this->assertEmpty($field->render($row));
}
}

View File

@ -2900,11 +2900,6 @@ parameters:
count: 1
path: modules/views/src/Plugin/views/field/RenderedEntity.php
-
message: "#^Variable \\$entity in isset\\(\\) always exists and is not nullable\\.$#"
count: 1
path: modules/views/src/Plugin/views/field/RenderedEntity.php
-
message: "#^Variable \\$items might not be defined\\.$#"
count: 1