From 7ea52649ec1bb0c75b5186bb383e255047245eb9 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Tue, 4 Apr 2023 14:37:06 +1000 Subject: [PATCH] =?UTF-8?q?Issue=20#3007424=20by=20acbramley,=20Spokje,=20?= =?UTF-8?q?mbovan,=20narendra.rajwar27,=20AaronBauman,=20ravi.shankar,=20a?= =?UTF-8?q?leevas,=20geek-merlin,=20jibran,=20YurkinPark,=20daffie,=20laro?= =?UTF-8?q?wlan,=20alexpott,=20marcelovani,=20catch,=20Berdir,=20Lendude,?= =?UTF-8?q?=20mstrelan,=20tar=5Finet,=20andrewbelcher,=20seanB,=20Honza=20?= =?UTF-8?q?Pobo=C5=99il,=20danflanagan8,=20susgo,=20yogeshmpawar,=20sylus,?= =?UTF-8?q?=20sjpeters79,=20raman.b:=20Multiple=20usages=20of=20FieldPlugi?= =?UTF-8?q?nBase::getEntity=20do=20not=20check=20for=20NULL,=20leading=20t?= =?UTF-8?q?o=20WSOD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Plugin/views/field/EntityLink.php | 5 +- .../src/Plugin/views/field/LinkApprove.php | 6 +- .../src/Plugin/views/field/LinkReply.php | 3 + .../Plugin/views/field/EntityLinkTest.php | 42 +++++++ .../Plugin/views/field/LinkApproveTest.php | 50 ++++++++ .../Unit/Plugin/views/field/LinkReplyTest.php | 50 ++++++++ .../src/Plugin/views/field/ContactLink.php | 9 +- .../tests/src/Unit/ContactLinkTest.php | 50 ++++++++ .../media_library/media_library.module | 4 +- .../views/field/MediaLibrarySelectForm.php | 4 + .../src/Unit/MediaLibrarySelectFormTest.php | 115 ++++++++++++++++++ .../src/Plugin/views/field/RevisionLink.php | 10 +- .../Plugin/views/field/RevisionLinkDelete.php | 3 + .../Plugin/views/field/RevisionLinkRevert.php | 3 + .../views/field/RevisionLinkDeleteTest.php | 50 ++++++++ .../views/field/RevisionLinkRevertTest.php | 50 ++++++++ .../Plugin/views/field/RevisionLinkTest.php | 50 ++++++++ .../src/Plugin/views/field/Permissions.php | 11 +- .../Plugin/views/field/PermissionsTest.php | 51 ++++++++ .../views/src/Plugin/views/field/BulkForm.php | 27 ++-- .../src/Plugin/views/field/EntityLink.php | 6 +- .../Plugin/views/field/FieldPluginBase.php | 19 ++- .../views/src/Plugin/views/field/LinkBase.php | 22 ++-- .../src/Plugin/views/field/RenderedEntity.php | 18 +-- .../tests/src/Traits/ViewsLoggerTestTrait.php | 36 ++++++ .../Unit/Plugin/views/field/BulkFormTest.php | 76 ++++++++++++ .../views/field/EntityOperationsUnitTest.php | 7 +- .../Unit/Plugin/views/field/LinkBaseTest.php | 76 ++++++++++++ .../Plugin/views/field/RenderedEntityTest.php | 46 +++++++ core/phpstan-baseline.neon | 5 - 30 files changed, 856 insertions(+), 48 deletions(-) create mode 100644 core/modules/comment/tests/src/Unit/Plugin/views/field/EntityLinkTest.php create mode 100644 core/modules/comment/tests/src/Unit/Plugin/views/field/LinkApproveTest.php create mode 100644 core/modules/comment/tests/src/Unit/Plugin/views/field/LinkReplyTest.php create mode 100644 core/modules/contact/tests/src/Unit/ContactLinkTest.php create mode 100644 core/modules/media_library/tests/src/Unit/MediaLibrarySelectFormTest.php create mode 100644 core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkDeleteTest.php create mode 100644 core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkRevertTest.php create mode 100644 core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkTest.php create mode 100644 core/modules/user/tests/src/Unit/Plugin/views/field/PermissionsTest.php create mode 100644 core/modules/views/tests/src/Traits/ViewsLoggerTestTrait.php create mode 100644 core/modules/views/tests/src/Unit/Plugin/views/field/BulkFormTest.php create mode 100644 core/modules/views/tests/src/Unit/Plugin/views/field/LinkBaseTest.php create mode 100644 core/modules/views/tests/src/Unit/Plugin/views/field/RenderedEntityTest.php diff --git a/core/modules/comment/src/Plugin/views/field/EntityLink.php b/core/modules/comment/src/Plugin/views/field/EntityLink.php index 9760057103f..6584f21318b 100644 --- a/core/modules/comment/src/Plugin/views/field/EntityLink.php +++ b/core/modules/comment/src/Plugin/views/field/EntityLink.php @@ -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']); } } diff --git a/core/modules/comment/src/Plugin/views/field/LinkApprove.php b/core/modules/comment/src/Plugin/views/field/LinkApprove.php index 683c3cbb4cf..86687f52f0f 100644 --- a/core/modules/comment/src/Plugin/views/field/LinkApprove.php +++ b/core/modules/comment/src/Plugin/views/field/LinkApprove.php @@ -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()]); } /** diff --git a/core/modules/comment/src/Plugin/views/field/LinkReply.php b/core/modules/comment/src/Plugin/views/field/LinkReply.php index 3ddbd081528..816f341a6c7 100644 --- a/core/modules/comment/src/Plugin/views/field/LinkReply.php +++ b/core/modules/comment/src/Plugin/views/field/LinkReply.php @@ -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(), diff --git a/core/modules/comment/tests/src/Unit/Plugin/views/field/EntityLinkTest.php b/core/modules/comment/tests/src/Unit/Plugin/views/field/EntityLinkTest.php new file mode 100644 index 00000000000..96d59bc77de --- /dev/null +++ b/core/modules/comment/tests/src/Unit/Plugin/views/field/EntityLinkTest.php @@ -0,0 +1,42 @@ +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)); + } + +} diff --git a/core/modules/comment/tests/src/Unit/Plugin/views/field/LinkApproveTest.php b/core/modules/comment/tests/src/Unit/Plugin/views/field/LinkApproveTest.php new file mode 100644 index 00000000000..b8e21dda921 --- /dev/null +++ b/core/modules/comment/tests/src/Unit/Plugin/views/field/LinkApproveTest.php @@ -0,0 +1,50 @@ +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)); + } + +} diff --git a/core/modules/comment/tests/src/Unit/Plugin/views/field/LinkReplyTest.php b/core/modules/comment/tests/src/Unit/Plugin/views/field/LinkReplyTest.php new file mode 100644 index 00000000000..ed48b8dabd4 --- /dev/null +++ b/core/modules/comment/tests/src/Unit/Plugin/views/field/LinkReplyTest.php @@ -0,0 +1,50 @@ +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)); + } + +} diff --git a/core/modules/contact/src/Plugin/views/field/ContactLink.php b/core/modules/contact/src/Plugin/views/field/ContactLink.php index a98be137ddd..af72aa3313d 100644 --- a/core/modules/contact/src/Plugin/views/field/ContactLink.php +++ b/core/modules/contact/src/Plugin/views/field/ContactLink.php @@ -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); diff --git a/core/modules/contact/tests/src/Unit/ContactLinkTest.php b/core/modules/contact/tests/src/Unit/ContactLinkTest.php new file mode 100644 index 00000000000..88304410326 --- /dev/null +++ b/core/modules/contact/tests/src/Unit/ContactLinkTest.php @@ -0,0 +1,50 @@ +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)); + } + +} diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index 6e8a8e58a34..b858450094a 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -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()]) : ''; } } } diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php index ce273305c60..fe3e6b6cf60 100644 --- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php @@ -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', [ diff --git a/core/modules/media_library/tests/src/Unit/MediaLibrarySelectFormTest.php b/core/modules/media_library/tests/src/Unit/MediaLibrarySelectFormTest.php new file mode 100644 index 00000000000..5a4863b22f0 --- /dev/null +++ b/core/modules/media_library/tests/src/Unit/MediaLibrarySelectFormTest.php @@ -0,0 +1,115 @@ +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]); + } + +} diff --git a/core/modules/node/src/Plugin/views/field/RevisionLink.php b/core/modules/node/src/Plugin/views/field/RevisionLink.php index 949ad6e07cc..b1b981c607c 100644 --- a/core/modules/node/src/Plugin/views/field/RevisionLink.php +++ b/core/modules/node/src/Plugin/views/field/RevisionLink.php @@ -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); diff --git a/core/modules/node/src/Plugin/views/field/RevisionLinkDelete.php b/core/modules/node/src/Plugin/views/field/RevisionLinkDelete.php index 2e1f683b529..8f92a2ee1b9 100644 --- a/core/modules/node/src/Plugin/views/field/RevisionLinkDelete.php +++ b/core/modules/node/src/Plugin/views/field/RevisionLinkDelete.php @@ -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()]); } diff --git a/core/modules/node/src/Plugin/views/field/RevisionLinkRevert.php b/core/modules/node/src/Plugin/views/field/RevisionLinkRevert.php index 33b20b88855..7c6c10f8b3c 100644 --- a/core/modules/node/src/Plugin/views/field/RevisionLinkRevert.php +++ b/core/modules/node/src/Plugin/views/field/RevisionLinkRevert.php @@ -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()]); } diff --git a/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkDeleteTest.php b/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkDeleteTest.php new file mode 100644 index 00000000000..d8b1b8183fa --- /dev/null +++ b/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkDeleteTest.php @@ -0,0 +1,50 @@ +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)); + } + +} diff --git a/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkRevertTest.php b/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkRevertTest.php new file mode 100644 index 00000000000..f6c189b1c2c --- /dev/null +++ b/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkRevertTest.php @@ -0,0 +1,50 @@ +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)); + } + +} diff --git a/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkTest.php b/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkTest.php new file mode 100644 index 00000000000..46966dff606 --- /dev/null +++ b/core/modules/node/tests/src/Unit/Plugin/views/field/RevisionLinkTest.php @@ -0,0 +1,50 @@ +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)); + } + +} diff --git a/core/modules/user/src/Plugin/views/field/Permissions.php b/core/modules/user/src/Plugin/views/field/Permissions.php index 705ba7a5892..c9b29a6e2d5 100644 --- a/core/modules/user/src/Plugin/views/field/Permissions.php +++ b/core/modules/user/src/Plugin/views/field/Permissions.php @@ -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; + } } } diff --git a/core/modules/user/tests/src/Unit/Plugin/views/field/PermissionsTest.php b/core/modules/user/tests/src/Unit/Plugin/views/field/PermissionsTest.php new file mode 100644 index 00000000000..0beff242bee --- /dev/null +++ b/core/modules/user/tests/src/Unit/Plugin/views/field/PermissionsTest.php @@ -0,0 +1,51 @@ +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); + } + +} diff --git a/core/modules/views/src/Plugin/views/field/BulkForm.php b/core/modules/views/src/Plugin/views/field/BulkForm.php index fe6a844e4b6..32afb60995b 100644 --- a/core/modules/views/src/Plugin/views/field/BulkForm.php +++ b/core/modules/views/src/Plugin/views/field/BulkForm.php @@ -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. diff --git a/core/modules/views/src/Plugin/views/field/EntityLink.php b/core/modules/views/src/Plugin/views/field/EntityLink.php index 6b22a408f02..3ebe91a109c 100644 --- a/core/modules/views/src/Plugin/views/field/EntityLink.php +++ b/core/modules/views/src/Plugin/views/field/EntityLink.php @@ -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); } diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index b13ef749dc8..7c26af31230 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -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; } /** diff --git a/core/modules/views/src/Plugin/views/field/LinkBase.php b/core/modules/views/src/Plugin/views/field/LinkBase.php index 468c7e5836d..83eec224723 100644 --- a/core/modules/views/src/Plugin/views/field/LinkBase.php +++ b/core/modules/views/src/Plugin/views/field/LinkBase.php @@ -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(); } } diff --git a/core/modules/views/src/Plugin/views/field/RenderedEntity.php b/core/modules/views/src/Plugin/views/field/RenderedEntity.php index 40a3a627554..8c96d87e6f9 100644 --- a/core/modules/views/src/Plugin/views/field/RenderedEntity.php +++ b/core/modules/views/src/Plugin/views/field/RenderedEntity.php @@ -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; } diff --git a/core/modules/views/tests/src/Traits/ViewsLoggerTestTrait.php b/core/modules/views/tests/src/Traits/ViewsLoggerTestTrait.php new file mode 100644 index 00000000000..bea149498d4 --- /dev/null +++ b/core/modules/views/tests/src/Traits/ViewsLoggerTestTrait.php @@ -0,0 +1,36 @@ +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); + } + +} diff --git a/core/modules/views/tests/src/Unit/Plugin/views/field/BulkFormTest.php b/core/modules/views/tests/src/Unit/Plugin/views/field/BulkFormTest.php new file mode 100644 index 00000000000..e7629346c6a --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/views/field/BulkFormTest.php @@ -0,0 +1,76 @@ +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]); + } + +} diff --git a/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php b/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php index 160a87eeff5..f844955f8fd 100644 --- a/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php @@ -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(); diff --git a/core/modules/views/tests/src/Unit/Plugin/views/field/LinkBaseTest.php b/core/modules/views/tests/src/Unit/Plugin/views/field/LinkBaseTest.php new file mode 100644 index 00000000000..897a86ae3fc --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/views/field/LinkBaseTest.php @@ -0,0 +1,76 @@ +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)); + } + +} diff --git a/core/modules/views/tests/src/Unit/Plugin/views/field/RenderedEntityTest.php b/core/modules/views/tests/src/Unit/Plugin/views/field/RenderedEntityTest.php new file mode 100644 index 00000000000..8639db20f9a --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/views/field/RenderedEntityTest.php @@ -0,0 +1,46 @@ +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)); + } + +} diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon index 6a18cc3136a..74689ff4658 100644 --- a/core/phpstan-baseline.neon +++ b/core/phpstan-baseline.neon @@ -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