From 6749e8de0a0c9940d88a8cf34e66f6c791f5b412 Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Thu, 26 Sep 2019 16:49:58 -0700 Subject: [PATCH] Issue #2969678 by seanB, oknate, phenaproxima, Wim Leers: Integrate Media Library with Content Moderation --- .../config/optional/views.view.media.yml | 41 +++ core/modules/media/media.module | 12 + core/modules/media/media.post_update.php | 69 ++++ core/modules/media/src/MediaViewsData.php | 10 + .../media/src/Plugin/views/filter/Status.php | 55 +++ .../tests/src/Functional/MediaAccessTest.php | 2 +- .../src/Functional/MediaOverviewPageTest.php | 56 ++- .../src/Functional/Update/MediaUpdateTest.php | 21 ++ .../install/views.view.media_library.yml | 41 +++ .../media_library.post_update.php | 67 ++++ .../src/MediaLibraryFieldWidgetOpener.php | 20 +- .../media_library/src/MediaLibraryState.php | 25 +- ...LibraryUpdateViewStatusExtraFilterTest.php | 47 +++ .../ContentModerationTest.php | 328 ++++++++++++++++++ 14 files changed, 741 insertions(+), 53 deletions(-) create mode 100644 core/modules/media/src/Plugin/views/filter/Status.php create mode 100644 core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateViewStatusExtraFilterTest.php create mode 100644 core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml index 2c8bec42b210..be380def0dde 100644 --- a/core/modules/media/config/optional/views.view.media.yml +++ b/core/modules/media/config/optional/views.view.media.yml @@ -749,6 +749,45 @@ display: plugin_id: boolean entity_type: media entity_field: status + status_extra: + id: status_extra + table: media_field_data + field: status_extra + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + operator_limit_selection: false + operator_list: { } + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: media + plugin_id: media_status langcode: id: langcode table: media_field_data @@ -833,6 +872,7 @@ display: - 'languages:language_interface' - url - url.query_args + - user - user.permissions tags: { } media_page_list: @@ -860,5 +900,6 @@ display: - 'languages:language_interface' - url - url.query_args + - user - user.permissions tags: { } diff --git a/core/modules/media/media.module b/core/modules/media/media.module index 7913600abcec..b6464144ec45 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -18,6 +18,7 @@ use Drupal\Core\Url; use Drupal\field\FieldConfigInterface; use Drupal\media\Plugin\media\Source\Image; use Drupal\media\Plugin\media\Source\OEmbedInterface; +use Drupal\views\ViewExecutable; /** * Implements hook_help(). @@ -539,3 +540,14 @@ function _media_type_add_form_submit(array &$form, FormStateInterface $form_stat $display->save(); } + +/** + * Implements hook_views_query_substitutions(). + */ +function media_views_query_substitutions(ViewExecutable $view) { + $account = \Drupal::currentUser(); + return [ + '***VIEW_OWN_UNPUBLISHED_MEDIA***' => (int) $account->hasPermission('view own unpublished media'), + '***ADMINISTER_MEDIA***' => (int) $account->hasPermission('administer media'), + ]; +} diff --git a/core/modules/media/media.post_update.php b/core/modules/media/media.post_update.php index f2a6c375f5f5..3bae4b448ef9 100644 --- a/core/modules/media/media.post_update.php +++ b/core/modules/media/media.post_update.php @@ -5,6 +5,9 @@ * Post update functions for Media. */ +use Drupal\user\RoleInterface; +use Drupal\views\Views; + /** * Clear caches due to changes in local tasks and action links. */ @@ -28,3 +31,69 @@ function media_post_update_enable_standalone_url() { $config->set('standalone_url', TRUE)->save(TRUE); } } + +/** + * Add a status extra filter to the media view default display. + */ +function media_post_update_add_status_extra_filter() { + $view = Views::getView('media'); + + if (!$view) { + return; + } + + // Fetch the filters from the default display and add the new 'status_extra' + // filter if it does not yet exist. + $default_display = $view->getDisplay(); + $filters = $default_display->getOption('filters'); + + if (!isset($filters['status_extra'])) { + $filters['status_extra'] = [ + 'group_info' => [ + 'widget' => 'select', + 'group_items' => [], + 'multiple' => FALSE, + 'description' => '', + 'default_group_multiple' => [], + 'default_group' => 'All', + 'label' => '', + 'identifier' => '', + 'optional' => TRUE, + 'remember' => FALSE, + ], + 'group' => 1, + 'relationship' => 'none', + 'exposed' => FALSE, + 'expose' => [ + 'use_operator' => FALSE, + 'remember' => FALSE, + 'operator_id' => '', + 'multiple' => FALSE, + 'description' => '', + 'required' => FALSE, + 'label' => '', + 'operator_limit_selection' => FALSE, + 'operator' => '', + 'identifier' => '', + 'operator_list' => [], + 'remember_roles' => [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID], + ], + 'entity_type' => 'media', + 'value' => '', + 'field' => 'status_extra', + 'is_grouped' => FALSE, + 'admin_label' => '', + 'operator' => '=', + 'table' => 'media_field_data', + 'plugin_id' => 'media_status', + 'id' => 'status_extra', + 'group_type' => 'group', + ]; + $default_display->setOption('filters', $filters); + $view->save(); + + return t("The 'Published status or admin user' filter was added to the %label view.", [ + '%label' => $view->storage->label(), + ]); + } +} diff --git a/core/modules/media/src/MediaViewsData.php b/core/modules/media/src/MediaViewsData.php index 7a038cf16e93..393548be84c6 100644 --- a/core/modules/media/src/MediaViewsData.php +++ b/core/modules/media/src/MediaViewsData.php @@ -18,6 +18,16 @@ class MediaViewsData extends EntityViewsData { $data['media_field_data']['table']['wizard_id'] = 'media'; $data['media_field_revision']['table']['wizard_id'] = 'media_revision'; + $data['media_field_data']['status_extra'] = [ + 'title' => $this->t('Published status or admin user'), + 'help' => $this->t('Filters out unpublished media if the current user cannot view it.'), + 'filter' => [ + 'field' => 'status', + 'id' => 'media_status', + 'label' => $this->t('Published status or admin user'), + ], + ]; + return $data; } diff --git a/core/modules/media/src/Plugin/views/filter/Status.php b/core/modules/media/src/Plugin/views/filter/Status.php new file mode 100644 index 000000000000..0ac00a0fdc8e --- /dev/null +++ b/core/modules/media/src/Plugin/views/filter/Status.php @@ -0,0 +1,55 @@ +ensureMyTable(); + $snippet = "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_MEDIA*** = 1) OR ***ADMINISTER_MEDIA*** = 1"; + if ($this->moduleHandler->moduleExists('content_moderation')) { + $snippet .= ' OR ***VIEW_ANY_UNPUBLISHED_NODES*** = 1'; + } + $this->query->addWhereExpression($this->options['group'], $snippet); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + $contexts[] = 'user'; + return $contexts; + } + +} diff --git a/core/modules/media/tests/src/Functional/MediaAccessTest.php b/core/modules/media/tests/src/Functional/MediaAccessTest.php index 7363c30a1662..050c567da84a 100644 --- a/core/modules/media/tests/src/Functional/MediaAccessTest.php +++ b/core/modules/media/tests/src/Functional/MediaAccessTest.php @@ -178,7 +178,7 @@ class MediaAccessTest extends MediaFunctionalTestBase { $this->drupalGet('admin/content'); $assert_session->linkByHrefExists('/admin/content/media'); $this->clickLink('Media'); - $this->assertCacheContext('user.permissions'); + $this->assertCacheContext('user'); $assert_session->statusCodeEquals(200); $assert_session->elementExists('css', '.view-media'); $assert_session->pageTextContains($this->loggedInUser->getDisplayName()); diff --git a/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php b/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php index a95515280f91..2f38619e7b61 100644 --- a/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php +++ b/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php @@ -32,7 +32,7 @@ class MediaOverviewPageTest extends MediaFunctionalTestBase { $assert_session->statusCodeEquals(403); $role = Role::load(RoleInterface::AUTHENTICATED_ID); $this->grantPermissions($role, ['access media overview']); - $this->drupalGet('/admin/content/media'); + $this->getSession()->reload(); $assert_session->statusCodeEquals(200); $assert_session->titleEquals('Media | Drupal'); $assert_session->fieldExists('Media name'); @@ -79,55 +79,45 @@ class MediaOverviewPageTest extends MediaFunctionalTestBase { ]); $media3->save(); - // Verify the view is now correctly populated. + // Verify the view is now correctly populated. The non-admin user can only + // view published media. $this->grantPermissions($role, [ 'view media', 'update any media', 'delete any media', ]); - $this->drupalGet('/admin/content/media'); + $this->getSession()->reload(); $row1 = $assert_session->elementExists('css', 'table tbody tr:nth-child(1)'); $row2 = $assert_session->elementExists('css', 'table tbody tr:nth-child(2)'); - $row3 = $assert_session->elementExists('css', 'table tbody tr:nth-child(3)'); // Media thumbnails. $assert_session->elementExists('css', 'td.views-field-thumbnail__target-id img', $row1); $assert_session->elementExists('css', 'td.views-field-thumbnail__target-id img', $row2); - $assert_session->elementExists('css', 'td.views-field-thumbnail__target-id img', $row3); // Media names. $name1 = $assert_session->elementExists('css', 'td.views-field-name a', $row1); $this->assertSame($media1->label(), $name1->getText()); $name2 = $assert_session->elementExists('css', 'td.views-field-name a', $row2); - $this->assertSame($media2->label(), $name2->getText()); - $name3 = $assert_session->elementExists('css', 'td.views-field-name a', $row3); - $this->assertSame($media3->label(), $name3->getText()); + $this->assertSame($media3->label(), $name2->getText()); $assert_session->linkByHrefExists('/media/' . $media1->id()); - $assert_session->linkByHrefExists('/media/' . $media2->id()); $assert_session->linkByHrefExists('/media/' . $media3->id()); // Media types. $type_element1 = $assert_session->elementExists('css', 'td.views-field-bundle', $row1); $this->assertSame($media_type1->label(), $type_element1->getText()); $type_element2 = $assert_session->elementExists('css', 'td.views-field-bundle', $row2); - $this->assertSame($media_type2->label(), $type_element2->getText()); - $type_element3 = $assert_session->elementExists('css', 'td.views-field-bundle', $row3); - $this->assertSame($media_type1->label(), $type_element3->getText()); + $this->assertSame($media_type1->label(), $type_element2->getText()); // Media authors. $author_element1 = $assert_session->elementExists('css', 'td.views-field-uid', $row1); $this->assertSame($this->adminUser->getDisplayName(), $author_element1->getText()); - $author_element2 = $assert_session->elementExists('css', 'td.views-field-uid', $row2); - $this->assertSame($this->adminUser->getDisplayName(), $author_element2->getText()); - $author_element3 = $assert_session->elementExists('css', 'td.views-field-uid', $row3); + $author_element3 = $assert_session->elementExists('css', 'td.views-field-uid', $row2); $this->assertSame($this->nonAdminUser->getDisplayName(), $author_element3->getText()); // Media publishing status. $status_element1 = $assert_session->elementExists('css', 'td.views-field-status', $row1); $this->assertSame('Published', $status_element1->getText()); - $status_element2 = $assert_session->elementExists('css', 'td.views-field-status', $row2); - $this->assertSame('Unpublished', $status_element2->getText()); - $status_element3 = $assert_session->elementExists('css', 'td.views-field-status', $row3); + $status_element3 = $assert_session->elementExists('css', 'td.views-field-status', $row2); $this->assertSame('Published', $status_element3->getText()); // Timestamp. @@ -142,6 +132,36 @@ class MediaOverviewPageTest extends MediaFunctionalTestBase { $delete_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.delete a', $row1); $this->assertSame('Delete', $delete_link1->getText()); $assert_session->linkByHrefExists('/media/' . $media1->id() . '/delete'); + + // Make the user the owner of the unpublished media item and assert the + // media item is only visible with the 'view own unpublished media' + // permission. + $media2->setOwner($this->nonAdminUser)->save(); + $this->getSession()->reload(); + $assert_session->pageTextNotContains($media2->label()); + $role->grantPermission('view own unpublished media')->save(); + $this->getSession()->reload(); + $row = $assert_session->elementExists('css', 'table tbody tr:nth-child(2)'); + $name = $assert_session->elementExists('css', 'td.views-field-name a', $row); + $this->assertSame($media2->label(), $name->getText()); + $status_element = $assert_session->elementExists('css', 'td.views-field-status', $row); + $this->assertSame('Unpublished', $status_element->getText()); + + // Assert the admin user can always view all media. + $this->drupalLogin($this->adminUser); + $this->drupalGet('/admin/content/media'); + $row1 = $assert_session->elementExists('css', 'table tbody tr:nth-child(1)'); + $row2 = $assert_session->elementExists('css', 'table tbody tr:nth-child(2)'); + $row3 = $assert_session->elementExists('css', 'table tbody tr:nth-child(3)'); + $name1 = $assert_session->elementExists('css', 'td.views-field-name a', $row1); + $this->assertSame($media1->label(), $name1->getText()); + $name2 = $assert_session->elementExists('css', 'td.views-field-name a', $row2); + $this->assertSame($media2->label(), $name2->getText()); + $name3 = $assert_session->elementExists('css', 'td.views-field-name a', $row3); + $this->assertSame($media3->label(), $name3->getText()); + $assert_session->linkByHrefExists('/media/' . $media1->id()); + $assert_session->linkByHrefExists('/media/' . $media2->id()); + $assert_session->linkByHrefExists('/media/' . $media3->id()); } } diff --git a/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php b/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php index ee97fe5ccd00..7e6c925bbde5 100644 --- a/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php +++ b/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php @@ -114,4 +114,25 @@ class MediaUpdateTest extends UpdatePathTestBase { $this->assertSession()->statusCodeEquals(200); } + /** + * Tests that the status extra filter is added to the media view. + * + * @see media_post_update_add_status_extra_filter() + */ + public function testMediaViewStatusExtraFilter() { + $config = $this->config('views.view.media'); + $this->assertNull($config->get('display.default.display_options.filters.status_extra')); + + $this->runUpdates(); + + $config = $this->config('views.view.media'); + $filter = $config->get('display.default.display_options.filters.status_extra'); + $this->assertInternalType('array', $filter); + $this->assertSame('status_extra', $filter['field']); + $this->assertSame('media', $filter['entity_type']); + $this->assertSame('media_status', $filter['plugin_id']); + $this->assertSame('status_extra', $filter['id']); + $this->assertFalse($filter['exposed']); + } + } diff --git a/core/modules/media_library/config/install/views.view.media_library.yml b/core/modules/media_library/config/install/views.view.media_library.yml index 2a47f38ecab5..5a0566722875 100644 --- a/core/modules/media_library/config/install/views.view.media_library.yml +++ b/core/modules/media_library/config/install/views.view.media_library.yml @@ -325,6 +325,45 @@ display: entity_type: media entity_field: bundle plugin_id: bundle + status_extra: + id: status_extra + table: media_field_data + field: status_extra + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + operator_limit_selection: false + operator_list: { } + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: media + plugin_id: media_status sorts: created: id: created @@ -395,6 +434,7 @@ display: - url - url.query_args - 'url.query_args:sort_by' + - user - user.permissions tags: { } page: @@ -700,6 +740,7 @@ display: - url - url.query_args - 'url.query_args:sort_by' + - user - user.permissions tags: { } # @todo Lock down access in https://www.drupal.org/node/2983179 diff --git a/core/modules/media_library/media_library.post_update.php b/core/modules/media_library/media_library.post_update.php index 979d0ed0db8b..aee481c4f656 100644 --- a/core/modules/media_library/media_library.post_update.php +++ b/core/modules/media_library/media_library.post_update.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\Entity\EntityFormMode; use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\image\Entity\ImageStyle; use Drupal\media\Entity\MediaType; +use Drupal\user\RoleInterface; use Drupal\views\Views; /** @@ -240,3 +241,69 @@ function media_library_post_update_add_media_library_image_style() { return t('The %label image style has been created successfully.', ['%label' => 'Media Library (220x220)']); } + +/** + * Add a status extra filter to the media library view default display. + */ +function media_library_post_update_add_status_extra_filter() { + $view = Views::getView('media_library'); + + if (!$view) { + return t('The media_library view could not be updated because it has been deleted. The Media Library module needs this view in order to work properly. Uninstall and reinstall the module so the view will be re-created.'); + } + + // Fetch the filters from the default display and add the new 'status_extra' + // filter if it does not yet exist. + $default_display = $view->getDisplay(); + $filters = $default_display->getOption('filters'); + + if (!isset($filters['status_extra'])) { + $filters['status_extra'] = [ + 'group_info' => [ + 'widget' => 'select', + 'group_items' => [], + 'multiple' => FALSE, + 'description' => '', + 'default_group_multiple' => [], + 'default_group' => 'All', + 'label' => '', + 'identifier' => '', + 'optional' => TRUE, + 'remember' => FALSE, + ], + 'group' => 1, + 'relationship' => 'none', + 'exposed' => FALSE, + 'expose' => [ + 'use_operator' => FALSE, + 'remember' => FALSE, + 'operator_id' => '', + 'multiple' => FALSE, + 'description' => '', + 'required' => FALSE, + 'label' => '', + 'operator_limit_selection' => FALSE, + 'operator' => '', + 'identifier' => '', + 'operator_list' => [], + 'remember_roles' => [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID], + ], + 'entity_type' => 'media', + 'value' => '', + 'field' => 'status_extra', + 'is_grouped' => FALSE, + 'admin_label' => '', + 'operator' => '=', + 'table' => 'media_field_data', + 'plugin_id' => 'media_status', + 'id' => 'status_extra', + 'group_type' => 'group', + ]; + $default_display->setOption('filters', $filters); + $view->save(); + + return t("The 'Published status or admin user' filter was added to the %label view.", [ + '%label' => $view->storage->label(), + ]); + } +} diff --git a/core/modules/media_library/src/MediaLibraryFieldWidgetOpener.php b/core/modules/media_library/src/MediaLibraryFieldWidgetOpener.php index 753c86b2c47d..b34b8d308b9c 100644 --- a/core/modules/media_library/src/MediaLibraryFieldWidgetOpener.php +++ b/core/modules/media_library/src/MediaLibraryFieldWidgetOpener.php @@ -38,10 +38,17 @@ class MediaLibraryFieldWidgetOpener implements MediaLibraryOpenerInterface { public function checkAccess(MediaLibraryState $state, AccountInterface $account) { $parameters = $state->getOpenerParameters() + ['entity_id' => NULL]; + $process_result = function ($result) { + if ($result instanceof RefinableCacheableDependencyInterface) { + $result->addCacheContexts(['url.query_args']); + } + return $result; + }; + // Forbid access if any of the required parameters are missing. foreach (['entity_type_id', 'bundle', 'field_name'] as $key) { if (empty($parameters[$key])) { - return AccessResult::forbidden("$key parameter is missing.")->addCacheableDependency($state); + return $process_result(AccessResult::forbidden("$key parameter is missing.")); } } @@ -69,10 +76,7 @@ class MediaLibraryFieldWidgetOpener implements MediaLibraryOpenerInterface { // If entity-level access is denied, there's no point in continuing. if (!$entity_access->isAllowed()) { - if ($entity_access instanceof RefinableCacheableDependencyInterface) { - $entity_access->addCacheableDependency($state); - } - return $entity_access; + return $process_result($entity_access); } // If the entity has not been loaded, create it in memory now. @@ -96,11 +100,7 @@ class MediaLibraryFieldWidgetOpener implements MediaLibraryOpenerInterface { } $field_access = $access_handler->fieldAccess('edit', $field_definition, $account, $items, TRUE); - $access = $entity_access->andIf($field_access); - if ($access instanceof RefinableCacheableDependencyInterface) { - $access->addCacheableDependency($state); - } - return $access; + return $process_result($entity_access->andIf($field_access)); } /** diff --git a/core/modules/media_library/src/MediaLibraryState.php b/core/modules/media_library/src/MediaLibraryState.php index 5de89162827c..258f19ded14f 100644 --- a/core/modules/media_library/src/MediaLibraryState.php +++ b/core/modules/media_library/src/MediaLibraryState.php @@ -3,8 +3,6 @@ namespace Drupal\media_library; use Drupal\Component\Utility\Crypt; -use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; @@ -44,7 +42,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; * subject to change in minor releases. External code should not instantiate * or extend this class. */ -class MediaLibraryState extends ParameterBag implements CacheableDependencyInterface { +class MediaLibraryState extends ParameterBag { /** * {@inheritdoc} @@ -276,25 +274,4 @@ class MediaLibraryState extends ParameterBag implements CacheableDependencyInter return $this->get('media_library_opener_parameters', []); } - /** - * {@inheritdoc} - */ - public function getCacheContexts() { - return ['url.query_args']; - } - - /** - * {@inheritdoc} - */ - public function getCacheMaxAge() { - return Cache::PERMANENT; - } - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return []; - } - } diff --git a/core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateViewStatusExtraFilterTest.php b/core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateViewStatusExtraFilterTest.php new file mode 100644 index 000000000000..29c42b8b91b1 --- /dev/null +++ b/core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateViewStatusExtraFilterTest.php @@ -0,0 +1,47 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz', + __DIR__ . '/../../../../../media/tests/fixtures/update/drupal-8.4.0-media_installed.php', + __DIR__ . '/../../../fixtures/update/drupal-8.7.2-media_library_installed.php', + ]; + } + + /** + * Tests that the status extra filter is added to the media library view. + * + * @see media_library_post_update_add_status_extra_filter() + */ + public function testMediaLibraryViewStatusExtraFilter() { + $config = $this->config('views.view.media_library'); + $this->assertNull($config->get('display.default.display_options.filters.status_extra')); + + $this->runUpdates(); + + $config = $this->config('views.view.media_library'); + $filter = $config->get('display.default.display_options.filters.status_extra'); + $this->assertInternalType('array', $filter); + $this->assertSame('status_extra', $filter['field']); + $this->assertSame('media', $filter['entity_type']); + $this->assertSame('media_status', $filter['plugin_id']); + $this->assertSame('status_extra', $filter['id']); + $this->assertFalse($filter['exposed']); + } + +} diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php new file mode 100644 index 000000000000..3e7361d10334 --- /dev/null +++ b/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php @@ -0,0 +1,328 @@ +createMediaType('image', ['id' => 'image']); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + // Create a media reference field on articles. + $this->createEntityReferenceField( + 'node', + 'article', + 'field_media', + 'Media', + 'media', + 'default', + ['target_bundles' => ['image']], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + // Add the media field to the form display. + $form_display = \Drupal::service('entity_display.repository')->getFormDisplay('node', 'article', 'default'); + $form_display->setComponent('field_media', [ + 'type' => 'media_library_widget', + ])->save(); + + // Configure the "Editorial" workflow to apply to image media. + $workflow = $this->createEditorialWorkflow(); + $workflow->getTypePlugin()->addEntityTypeAndBundle('media', 'image'); + $workflow->save(); + + $image = File::create([ + 'uri' => $this->getTestFiles('image')[0]->uri, + ]); + $image->setPermanent(); + $image->save(); + + // Create a draft, published and archived media item. + $draft_media = Media::create([ + 'name' => 'Hoglet', + 'bundle' => 'image', + 'field_media_image' => $image, + 'moderation_state' => 'draft', + ]); + $draft_media->save(); + $published_media = Media::create([ + 'name' => 'Panda', + 'bundle' => 'image', + 'field_media_image' => $image, + 'moderation_state' => 'published', + ]); + $published_media->save(); + $archived_media = Media::create([ + 'name' => 'Mammoth', + 'bundle' => 'image', + 'field_media_image' => $image, + 'moderation_state' => 'archived', + ]); + $archived_media->save(); + + // Create some users for our tests. We want to check with user 1, a media + // administrator with 'administer media' permissions, a user that has the + // 'view media' permissions, a user that can 'view media' and 'view own + // unpublished media', and a user that has 'view media' and 'view any + // unpublished content' permissions. + $this->userAdmin = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'access media overview', + 'edit own article content', + 'create article content', + 'administer media', + ]); + $this->userViewer = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'access media overview', + 'edit own article content', + 'create article content', + 'view media', + 'create media', + ]); + $this->userViewOwnUnpublished = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'access media overview', + 'edit own article content', + 'create article content', + 'view media', + 'view own unpublished media', + 'create media', + ]); + $this->userViewAnyUnpublished = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'access media overview', + 'edit own article content', + 'create article content', + 'view media', + 'create media', + 'view any unpublished content', + ]); + } + + /** + * Tests the media library widget only shows published media. + */ + public function testAdministrationPage() { + // User 1 should be able to see all media items. + $this->drupalLogin($this->rootUser); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + + // The media admin user should be able to see all media items. + $this->drupalLogin($this->userAdmin); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + + // The media viewer user should be able to see only published media items. + $this->drupalLogin($this->userViewer); + $this->drupalGet('admin/content/media'); + $this->assertOnlyPublishedMedia(); + + // The media viewer user that can also view its own unpublished media should + // also be able to see only published media items since it is not the owner + // of the created media items. + $this->drupalLogin($this->userViewOwnUnpublished); + $this->drupalGet('admin/content/media'); + $this->assertOnlyPublishedMedia(); + + // When content moderation is enabled, a media viewer that can view any + // unpublished content should be able to see all media. + // @see content_moderation_entity_access() + $this->drupalLogin($this->userViewAnyUnpublished); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + + // Assign all media to the user with the 'view own unpublished media' + // permission. + foreach (Media::loadMultiple() as $media) { + $media->setOwner($this->userViewOwnUnpublished); + $media->save(); + } + + // User 1 should still be able to see all media items. + $this->drupalLogin($this->rootUser); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + + // The media admin user should still be able to see all media items. + $this->drupalLogin($this->userAdmin); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + + // The media viewer user should still be able to see only published media + // items. + $this->drupalLogin($this->userViewer); + $this->drupalGet('admin/content/media'); + $this->assertOnlyPublishedMedia(); + + // The media viewer user that can also view its own unpublished media + // should now be able to see all media items since it is the owner of the + // created media items. + $this->drupalLogin($this->userViewOwnUnpublished); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + + // The media viewer that can view any unpublished content should still be + // able to see all media. + $this->drupalLogin($this->userViewAnyUnpublished); + $this->drupalGet('admin/content/media'); + $this->assertAllMedia(); + } + + /** + * Tests the media library widget only shows published media. + */ + public function testWidget() { + $assert_session = $this->assertSession(); + + // All users should only be able to see published media items. + $this->drupalLogin($this->rootUser); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userAdmin); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userViewer); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userViewOwnUnpublished); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userViewAnyUnpublished); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + + // After we change the owner to the user with 'view own unpublished media' + // permission, all users should still only be able to see published media. + foreach (Media::loadMultiple() as $media) { + $media->setOwner($this->userViewOwnUnpublished); + $media->save(); + } + + $this->drupalLogin($this->rootUser); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userAdmin); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userViewer); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userViewOwnUnpublished); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + $this->drupalLogin($this->userViewAnyUnpublished); + $this->drupalGet('node/add/article'); + $assert_session->elementExists('css', '.media-library-open-button[name^="field_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertOnlyPublishedMedia(); + } + + /** + * Asserts all media items are visible. + */ + protected function assertAllMedia() { + $assert_session = $this->assertSession(); + $assert_session->pageTextContains('Hoglet'); + $assert_session->pageTextContains('Panda'); + $assert_session->pageTextContains('Mammoth'); + } + + /** + * Asserts only published media items are visible. + */ + protected function assertOnlyPublishedMedia() { + $assert_session = $this->assertSession(); + $assert_session->pageTextNotContains('Hoglet'); + $assert_session->pageTextContains('Panda'); + $assert_session->pageTextNotContains('Mammoth'); + } + +}