Commit 062c2556 authored by Gábor Hojtsy's avatar Gábor Hojtsy

Issue #2885486 by seanB, timmillwood, tstoeckler, Wim Leers: Media entity is...

Issue #2885486 by seanB, timmillwood, tstoeckler, Wim Leers: Media entity is revisionable but doesn't have a revision link template
parent 1364bb63
......@@ -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.'
......@@ -4,3 +4,18 @@ 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\Core\Entity\Controller\EntityViewController::viewRevision'
_title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
options:
parameters:
media:
type: entity:media
media_revision:
type: entity_revision:media
requirements:
_access_media_revision: 'view'
media: \d+
......@@ -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 }
<?php
namespace Drupal\media\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\media\MediaInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an access checker for media item revisions.
*
* @ingroup media_access
*/
class MediaRevisionAccessCheck implements AccessInterface {
/**
* The media storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $mediaStorage;
/**
* The media access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $mediaAccess;
/**
* A static cache of access checks.
*
* @var array
*/
protected $access = [];
/**
* Constructs a new MediaRevisionAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->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') {
if (!$media || $op !== 'view') {
// 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('view all media revisions') && !$account->hasPermission('administer media')) {
$this->access[$cid] = FALSE;
return FALSE;
}
// There should be at least two revisions. If the revision ID of the
// given media item and the revision ID 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->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];
}
/**
* Counts the number of revisions in the default language.
*
* @param \Drupal\media\MediaInterface $media
* The media item for which to to count the revisions.
*
* @return int
* The number of revisions in the default language.
*/
protected function countDefaultLanguageRevisions(MediaInterface $media) {
$entity_type = $media->getEntityType();
$count = $this->mediaStorage->getQuery()
->allRevisions()
->condition($entity_type->getKey('id'), $media->id())
->condition($entity_type->getKey('default_langcode'), 1)
->count()
->execute();
return $count;
}
}
......@@ -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}"
* }
* )
......
......@@ -4,6 +4,9 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\media\MediaInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the revisionability of media entities.
......@@ -12,6 +15,62 @@
*/
class MediaRevisionTest extends MediaFunctionalTestBase {
/**
* Checks media revision operations.
*/
public function testRevisions() {
$assert = $this->assertSession();
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $media_storage */
$media_storage = $this->container->get('entity.manager')->getStorage('media');
// Create a media type and media item.
$media_type = $this->createMediaType();
$media = $media_storage->create([
'bundle' => $media_type->id(),
'name' => 'Unnamed',
]);
$media->save();
// You can't access the revision page when there is only 1 revision.
$this->drupalGet('media/' . $media->id() . '/revisions/' . $media->getRevisionId() . '/view');
$assert->statusCodeEquals(403);
// Create some revisions.
$media_revisions = [];
$media_revisions[] = clone $media;
$revision_count = 3;
for ($i = 0; $i < $revision_count; $i++) {
$media->revision_log = $this->randomMachineName(32);
$media = $this->createMediaRevision($media);
$media_revisions[] = clone $media;
}
// Get the last revision for simple checks.
/** @var \Drupal\media\MediaInterface $media */
$media = end($media_revisions);
// Test permissions.
$this->drupalLogin($this->nonAdminUser);
/** @var \Drupal\user\RoleInterface $role */
$role = Role::load(RoleInterface::AUTHENTICATED_ID);
// Test 'view all media revisions' permission ('view media' permission is
// needed as well).
user_role_revoke_permissions($role->id(), ['view media', 'view all media revisions']);
$this->drupalGet('media/' . $media->id() . '/revisions/' . $media->getRevisionId() . '/view');
$assert->statusCodeEquals(403);
$this->grantPermissions($role, ['view media', 'view all media revisions']);
$this->drupalGet('media/' . $media->id() . '/revisions/' . $media->getRevisionId() . '/view');
$assert->statusCodeEquals(200);
// Confirm the revision page shows the correct title.
$assert->pageTextContains($media->label());
// Confirm that the last revision is the default revision.
$this->assertTrue($media->isDefaultRevision(), 'Last revision is the default.');
}
/**
* Tests creating revisions of a File media item.
*/
......@@ -43,6 +102,13 @@ public function testFileMediaRevision() {
$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');
}
/**
......@@ -83,6 +149,29 @@ 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');
}
/**
* Creates a new revision for a given media item.
*
* @param \Drupal\media\MediaInterface $media
* A media object.
*
* @return \Drupal\media\MediaInterface
* A media object with up to date revision information.
*/
protected function createMediaRevision(MediaInterface $media) {
$media->set('name', $this->randomMachineName());
$media->setNewRevision();
$media->save();
return $media;
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment