diff --git a/core/modules/media/media.permissions.yml b/core/modules/media/media.permissions.yml index 530365e1f4..79eadb5058 100644 --- a/core/modules/media/media.permissions.yml +++ b/core/modules/media/media.permissions.yml @@ -23,3 +23,7 @@ delete any media: create media: title: 'Create media' + +view all media revisions: + title: 'View all media revisions' + description: 'To view a revision, you also need permission to view the media item.' diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml index 67ea1fb81c..c87fcad9ea 100644 --- a/core/modules/media/media.routing.yml +++ b/core/modules/media/media.routing.yml @@ -4,3 +4,12 @@ entity.media.multiple_delete_confirm: _form: '\Drupal\media\Form\MediaDeleteMultipleConfirmForm' requirements: _permission: 'administer media+delete any media' + +entity.media.revision: + path: '/media/{media}/revisions/{media_revision}/view' + defaults: + _controller: '\Drupal\media\Controller\MediaController::revisionShow' + _title_callback: '\Drupal\media\Controller\MediaController::revisionPageTitle' + requirements: + _access_media_revision: 'view' + media: \d+ diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml index a286b528d9..f22f90a124 100644 --- a/core/modules/media/media.services.yml +++ b/core/modules/media/media.services.yml @@ -2,3 +2,9 @@ services: plugin.manager.media.source: class: Drupal\media\MediaSourceManager parent: default_plugin_manager + + access_check.media.revision: + class: Drupal\media\Access\MediaRevisionAccessCheck + arguments: ['@entity_type.manager'] + tags: + - { name: access_check, applies_to: _access_media_revision } diff --git a/core/modules/media/src/Access/MediaRevisionAccessCheck.php b/core/modules/media/src/Access/MediaRevisionAccessCheck.php new file mode 100644 index 0000000000..35228a8b18 --- /dev/null +++ b/core/modules/media/src/Access/MediaRevisionAccessCheck.php @@ -0,0 +1,135 @@ +mediaStorage = $entity_type_manager->getStorage('media'); + $this->mediaAccess = $entity_type_manager->getAccessControlHandler('media'); + } + + /** + * Checks routing access for the media item revision. + * + * @param \Symfony\Component\Routing\Route $route + * The route to check against. + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * @param int $media_revision + * (optional) The media item revision ID. If not specified, but $media is, + * access is checked for that object's revision. + * @param \Drupal\media\MediaInterface $media + * (optional) A media item. Used for checking access to a media items + * default revision when $media_revision is unspecified. Ignored when + * $media_revision is specified. If neither $media_revision nor $media are + * specified, then access is denied. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(Route $route, AccountInterface $account, $media_revision = NULL, MediaInterface $media = NULL) { + if ($media_revision) { + $media = $this->mediaStorage->loadRevision($media_revision); + } + $operation = $route->getRequirement('_access_media_revision'); + return AccessResult::allowedIf($media && $this->checkAccess($media, $account, $operation))->cachePerPermissions()->addCacheableDependency($media); + } + + /** + * Checks media item revision access. + * + * @param \Drupal\media\MediaInterface $media + * The media item to check. + * @param \Drupal\Core\Session\AccountInterface $account + * A user object representing the user for whom the operation is to be + * performed. + * @param string $op + * (optional) The specific operation being checked. Defaults to 'view'. + * + * @return bool + * TRUE if the operation may be performed, FALSE otherwise. + */ + public function checkAccess(MediaInterface $media, AccountInterface $account, $op = 'view') { + $map = [ + 'view' => 'view all media revisions', + ]; + + if (!$media || !isset($map[$op])) { + // If there was no media to check against, or the $op was not one of the + // supported ones, we return access denied. + return FALSE; + } + + // Statically cache access by revision ID, language code, user account ID, + // and operation. + $langcode = $media->language()->getId(); + $cid = $media->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op; + + if (!isset($this->access[$cid])) { + // Perform basic permission checks first. + if (!$account->hasPermission($map[$op]) && !$account->hasPermission('administer media')) { + $this->access[$cid] = FALSE; + return FALSE; + } + + // There should be at least two revisions. If the vid of the given media + // item and the vid of the default revision differ, then we already have + // two different revisions so there is no need for a separate database + // check. + if ($media->isDefaultRevision() && ($this->mediaStorage->countDefaultLanguageRevisions($media) == 1)) { + $this->access[$cid] = FALSE; + } + elseif ($account->hasPermission('administer media')) { + $this->access[$cid] = TRUE; + } + else { + // First check the access to the default revision and finally, if the + // media passed in is not the default revision then access to that, too. + $this->access[$cid] = $this->mediaAccess->access($this->mediaStorage->load($media->id()), $op, $account) && ($media->isDefaultRevision() || $this->mediaAccess->access($media, $op, $account)); + } + } + + return $this->access[$cid]; + } + +} diff --git a/core/modules/media/src/Controller/MediaController.php b/core/modules/media/src/Controller/MediaController.php new file mode 100644 index 0000000000..7d500ffce0 --- /dev/null +++ b/core/modules/media/src/Controller/MediaController.php @@ -0,0 +1,98 @@ +entityManager = $entity_manager; + $this->dateFormatter = $date_formatter; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('date.formatter'), + $container->get('renderer') + ); + } + + /** + * Displays a media item revision. + * + * @param int $media_revision + * The media item revision ID. + * + * @return array + * An array suitable for drupal_render(). + */ + public function revisionShow($media_revision) { + $media = $this->entityManager->getStorage('media')->loadRevision($media_revision); + $media = $this->entityManager->getTranslationFromContext($media); + $media_view_controller = new EntityViewController($this->entityManager, $this->renderer); + $page = $media_view_controller->view($media); + unset($page['media'][$media->id()]['#cache']); + return $page; + } + + /** + * Page title callback for a media revision. + * + * @param int $media_revision + * The media revision ID. + * + * @return string + * The page title. + */ + public function revisionPageTitle($media_revision) { + $media = $this->entityTypeManager->getStorage('media')->loadRevision($media_revision); + return $this->t('Revision of %title from %date', ['%title' => $media->label(), '%date' => $this->dateFormatter->format($media->getRevisionCreationTime())]); + } + +} diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index 858404112e..f8713f8f56 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -29,7 +29,7 @@ * ), * bundle_label = @Translation("Media type"), * handlers = { - * "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage", + * "storage" = "Drupal\media\MediaStorage", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\Core\Entity\EntityListBuilder", * "access" = "Drupal\media\MediaAccessControlHandler", @@ -76,6 +76,7 @@ * "canonical" = "/media/{media}", * "delete-form" = "/media/{media}/delete", * "edit-form" = "/media/{media}/edit", + * "revision" = "/media/{media}/revisions/{media_revision}/view", * "admin-form" = "/admin/structure/media/manage/{media_type}" * } * ) diff --git a/core/modules/media/src/MediaStorage.php b/core/modules/media/src/MediaStorage.php new file mode 100644 index 0000000000..2f7aac2e20 --- /dev/null +++ b/core/modules/media/src/MediaStorage.php @@ -0,0 +1,22 @@ +database->query('SELECT COUNT(*) FROM {media_field_revision} WHERE mid = :mid AND default_langcode = 1', [':mid' => $media->id()])->fetchField(); + } + +} diff --git a/core/modules/media/src/MediaStorageInterface.php b/core/modules/media/src/MediaStorageInterface.php new file mode 100644 index 0000000000..33adebde7f --- /dev/null +++ b/core/modules/media/src/MediaStorageInterface.php @@ -0,0 +1,23 @@ +fillField('Name', 'Foobaz'); $page->pressButton('Save and keep published'); $this->assertRevisionCount($media, 2); + + // Confirm the correct revision title appears on "view revisions" page. + $media = $this->container->get('entity_type.manager') + ->getStorage('media') + ->loadUnchanged(1); + $this->drupalGet("media/" . $media->id() . "/revisions/" . $media->getRevisionId() . "/view"); + $assert->pageTextContains('Foobaz'); } /** @@ -83,6 +90,13 @@ public function testImageMediaRevision() { $page->fillField('Name', 'Foobaz'); $page->pressButton('Save and keep published'); $this->assertRevisionCount($media, 2); + + // Confirm the correct revision title appears on "view revisions" page. + $media = $this->container->get('entity_type.manager') + ->getStorage('media') + ->loadUnchanged(1); + $this->drupalGet("media/" . $media->id() . "/revisions/" . $media->getRevisionId() . "/view"); + $assert->pageTextContains('Foobaz'); } /**