Commit b77bf6a9 authored by xjm's avatar xjm
Browse files

Issue #3025867 by tim.plunkett, Kristen Pol, phenaproxima, alexpott, xjm,...

Issue #3025867 by tim.plunkett, Kristen Pol, phenaproxima, alexpott, xjm, andypost: Provide a classed object for TempStore metadata
parent c594d13e
<?php
namespace Drupal\Core\TempStore\Element;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a link to break a tempstore lock.
*
* Properties:
* - #label: The label of the object that is locked.
* - #lock: \Drupal\Core\TempStore\Lock object.
* - #url: \Drupal\Core\Url object pointing to the break lock form.
*
* Usage example:
* @code
* $build['examples_lock'] = [
* '#type' => 'break_lock_link',
* '#label' => $this->t('example item'),
* '#lock' => $tempstore->getMetadata('example_key'),
* '#url' => \Drupal\Core\Url::fromRoute('examples.break_lock_form'),
* ];
* @endcode
*
* @RenderElement("break_lock_link")
*/
class BreakLockLink extends RenderElement implements ContainerFactoryPluginInterface {
/**
* The date formatter.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new BreakLockLink.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, DateFormatterInterface $date_formatter, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->dateFormatter = $date_formatter;
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('date.formatter'),
$container->get('entity_type.manager'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#pre_render' => [
[$this, 'preRenderLock'],
],
];
}
/**
* Pre-render callback: Renders a lock into #markup.
*
* @param array $element
* A structured array with the following keys:
* - #label: The label of the object that is locked.
* - #lock: The lock object.
* - #url: The URL object with the destination to the break lock form.
*
* @return array
* The passed-in element containing a rendered lock in '#markup'.
*/
public function preRenderLock($element) {
if (isset($element['#lock']) && isset($element['#label']) && isset($element['#url'])) {
/** @var \Drupal\Core\TempStore\Lock $lock */
$lock = $element['#lock'];
$age = $this->dateFormatter->formatTimeDiffSince($lock->getUpdated());
$owner = $this->entityTypeManager->getStorage('user')->load($lock->getOwnerId());
$username = [
'#theme' => 'username',
'#account' => $owner,
];
$element['#markup'] = $this->t('This @label is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to <a href=":url">break this lock</a>.', [
'@label' => $element['#label'],
'@user' => $this->renderer->render($username),
'@age' => $age,
':url' => $element['#url']->toString(),
]);
}
return $element;
}
}
<?php
namespace Drupal\Core\TempStore;
/**
* Provides a value object representing the lock from a TempStore.
*/
final class Lock {
/**
* The owner ID.
*
* @var int
*/
private $ownerId;
/**
* The timestamp the lock was last updated.
*
* @var int
*/
private $updated;
/**
* Constructs a new Lock object.
*
* @param int $owner_id
* The owner ID.
* @param int $updated
* The updated timestamp.
*/
public function __construct($owner_id, $updated) {
$this->ownerId = $owner_id;
$this->updated = $updated;
}
/**
* Gets the owner ID.
*
* @return int
* The owner ID.
*/
public function getOwnerId() {
return $this->ownerId;
}
/**
* Gets the timestamp of the last update to the lock.
*
* @return int
* The updated timestamp.
*/
public function getUpdated() {
return $this->updated;
}
/**
* Provides backwards compatibility for using the lock as a \stdClass object.
*/
public function __get($name) {
if ($name === 'owner') {
@trigger_error('Using the "owner" public property of a TempStore lock is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\Core\TempStore\Lock::getOwnerId() instead. See https://www.drupal.org/node/3025869.', E_USER_DEPRECATED);
return $this->getOwnerId();
}
if ($name === 'updated') {
@trigger_error('Using the "updated" public property of a TempStore lock is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\Core\TempStore\Lock::getUpdated() instead. See https://www.drupal.org/node/3025869.', E_USER_DEPRECATED);
return $this->getUpdated();
}
throw new \InvalidArgumentException($name);
}
}
......@@ -155,7 +155,7 @@ public function set($key, $value) {
* @param string $key
* The key of the data to store.
*
* @return mixed
* @return \Drupal\Core\TempStore\Lock|null
* An object with the owner and updated time if the key has a value, or
* NULL otherwise.
*/
......@@ -166,7 +166,7 @@ public function getMetadata($key) {
if ($object) {
// Don't keep the data itself in memory.
unset($object->data);
return $object;
return new Lock($object->owner, $object->updated);
}
}
......
......@@ -215,7 +215,7 @@ public function set($key, $value) {
* @param string $key
* The key of the data to store.
*
* @return mixed
* @return \Drupal\Core\TempStore\Lock|null
* An object with the owner and updated time if the key has a value, or
* NULL otherwise.
*/
......@@ -225,7 +225,7 @@ public function getMetadata($key) {
if ($object) {
// Don't keep the data itself in memory.
unset($object->data);
return $object;
return new Lock($object->owner, $object->updated);
}
}
......
......@@ -189,7 +189,7 @@ public function testGetMetadata() {
->will($this->returnValue(FALSE));
$metadata = $this->tempStore->getMetadata('test');
$this->assertObjectHasAttribute('owner', $metadata);
$this->assertObjectHasAttribute('updated', $metadata);
// Data should get removed.
$this->assertObjectNotHasAttribute('data', $metadata);
......
......@@ -275,7 +275,7 @@ public function testGetMetadata() {
->will($this->returnValue(FALSE));
$metadata = $this->tempStore->getMetadata('test');
$this->assertObjectHasAttribute('owner', $metadata);
$this->assertObjectHasAttribute('updated', $metadata);
// Data should get removed.
$this->assertObjectNotHasAttribute('data', $metadata);
......
......@@ -71,7 +71,7 @@ public function getQuestion() {
*/
public function getDescription() {
$locked = $this->tempStore->getMetadata($this->entity->id());
$account = $this->entityManager->getStorage('user')->load($locked->owner);
$account = $this->entityManager->getStorage('user')->load($locked->getOwnerId());
$username = [
'#theme' => 'username',
'#account' => $account,
......
......@@ -89,7 +89,7 @@ public function convert($value, $definition, $name, array $defaults) {
else {
$view->disable();
}
$view->lock = $store->getMetadata($value);
$view->setLock($store->getMetadata($value));
}
// Otherwise, decorate the existing view for use in the UI.
else {
......
......@@ -87,6 +87,7 @@ public static function create(ContainerInterface $container) {
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\views_ui\ViewUI $view */
$view = $this->entity;
$display_id = $this->displayID;
// Do not allow the form to be cached, because $form_state->get('view') can become
......@@ -127,20 +128,16 @@ public function form(array $form, FormStateInterface $form_state) {
$form['#attributes']['class'] = ['form-edit'];
if ($view->isLocked()) {
$username = [
'#theme' => 'username',
'#account' => $this->entityManager->getStorage('user')->load($view->lock->owner),
];
$lock_message_substitutions = [
'@user' => \Drupal::service('renderer')->render($username),
'@age' => $this->dateFormatter->formatTimeDiffSince($view->lock->updated),
':url' => $view->toUrl('break-lock-form')->toString(),
];
$form['locked'] = [
'#type' => 'container',
'#attributes' => ['class' => ['view-locked', 'messages', 'messages--warning']],
'#children' => $this->t('This view is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to <a href=":url">break this lock</a>.', $lock_message_substitutions),
'#weight' => -10,
'message' => [
'#type' => 'break_lock_link',
'#label' => $view->getEntityType()->getSingularLabel(),
'#lock' => $view->getLock(),
'#url' => $view->toUrl('break-lock-form'),
],
];
}
else {
......
......@@ -6,6 +6,7 @@
use Drupal\Component\Utility\Timer;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\Lock;
use Drupal\views\Views;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\views\ViewExecutable;
......@@ -48,12 +49,14 @@ class ViewUI implements ViewEntityInterface {
* If this view is locked for editing.
*
* If this view is locked it will contain the result of
* \Drupal\Core\TempStore\SharedTempStore::getMetadata(). Which can be a stdClass or
* NULL.
* \Drupal\Core\TempStore\SharedTempStore::getMetadata().
*
* @var object
* For backwards compatibility, public access to this property is provided by
* ::__set() and ::__get().
*
* @var \Drupal\Core\TempStore\Lock|null
*/
public $lock;
private $lock;
/**
* If this view has been changed.
......@@ -888,7 +891,8 @@ public function cacheSet() {
* TRUE if the view is locked, FALSE otherwise.
*/
public function isLocked() {
return is_object($this->lock) && ($this->lock->owner != \Drupal::currentUser()->id());
$lock = $this->getLock();
return $lock && $lock->getOwnerId() != \Drupal::currentUser()->id();
}
/**
......@@ -1351,4 +1355,63 @@ public function addCacheTags(array $cache_tags) {
return $this->storage->addCacheTags($cache_tags);
}
/**
* Gets the lock on this View.
*
* @return \Drupal\Core\TempStore\Lock|null
* The lock, if one exists.
*/
public function getLock() {
return $this->lock;
}
/**
* Sets a lock on this View.
*
* @param \Drupal\Core\TempStore\Lock $lock
* The lock object.
*
* @return $this
*/
public function setLock(Lock $lock) {
$this->lock = $lock;
return $this;
}
/**
* Unsets the lock on this View.
*
* @return $this
*/
public function unsetLock() {
$this->lock = NULL;
return $this;
}
/**
* {@inheritdoc}
*/
public function __set($name, $value) {
if ($name === 'lock') {
@trigger_error('Using the "lock" public property of a View is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\views_ui\ViewUI::setLock() instead. See https://www.drupal.org/node/3025869.', E_USER_DEPRECATED);
if ($value instanceof \stdClass && property_exists($value, 'owner') && property_exists($value, 'updated')) {
$value = new Lock($value->owner, $value->updated);
}
$this->setLock($value);
}
else {
$this->{$name} = $value;
}
}
/**
* {@inheritdoc}
*/
public function __get($name) {
if ($name === 'lock') {
@trigger_error('Using the "lock" public property of a View is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\views_ui\ViewUI::getLock() instead. See https://www.drupal.org/node/3025869.', E_USER_DEPRECATED);
return $this->getLock();
}
}
}
......@@ -30,13 +30,13 @@ public function testCacheData() {
// Make sure we have 'changes' to the view.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/default/title', [], t('Apply'));
$this->assertText('You have unsaved changes.');
$this->assertEqual($temp_store->getMetadata('test_view')->owner, $views_admin_user_uid, 'View cache has been saved.');
$this->assertEqual($temp_store->getMetadata('test_view')->getOwnerId(), $views_admin_user_uid, 'View cache has been saved.');
$view_cache = $temp_store->get('test_view');
// The view should be enabled.
$this->assertTrue($view_cache->status(), 'The view is enabled.');
// The view should now be locked.
$this->assertEqual($temp_store->getMetadata('test_view')->owner, $views_admin_user_uid, 'The view is locked.');
$this->assertEqual($temp_store->getMetadata('test_view')->getOwnerId(), $views_admin_user_uid, 'The view is locked.');
// Cancel the view edit and make sure the cache is deleted.
$this->drupalPostForm(NULL, [], t('Cancel'));
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\views_ui\Unit;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\TempStore\Lock;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Entity\View;
use Drupal\views_ui\ViewUI;
......@@ -90,6 +91,47 @@ public function testIsLocked() {
// A view_ui without a lock object is not locked.
$this->assertFalse($view_ui->isLocked());
// Set the lock object with a different owner than the mocked account above.
$lock = new Lock(2, (int) $_SERVER['REQUEST_TIME']);
$view_ui->setLock($lock);
$this->assertTrue($view_ui->isLocked());
// Set a different lock object with the same object as the mocked account.
$lock = new Lock(1, (int) $_SERVER['REQUEST_TIME']);
$view_ui->setLock($lock);
$this->assertFalse($view_ui->isLocked());
$view_ui->unsetLock(NULL);
$this->assertFalse($view_ui->isLocked());
}
/**
* Tests the isLocked method.
*
* @expectedDeprecation Using the "lock" public property of a View is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\views_ui\ViewUI::setLock() instead. See https://www.drupal.org/node/3025869.
* @group legacy
*/
public function testIsLockedLegacy() {
$storage = $this->getMock('Drupal\views\Entity\View', [], [[], 'view']);
$executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
->disableOriginalConstructor()
->setConstructorArgs([$storage])
->getMock();
$storage->set('executable', $executable);
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$account->expects($this->exactly(2))
->method('id')
->will($this->returnValue(1));
$container = new ContainerBuilder();
$container->set('current_user', $account);
\Drupal::setContainer($container);
$view_ui = new ViewUI($storage);
// A view_ui without a lock object is not locked.
$this->assertFalse($view_ui->isLocked());
// Set the lock object with a different owner than the mocked account above.
$lock = (object) [
'owner' => 2,
......
......@@ -60,13 +60,13 @@ public function testAnonymousCanUsePrivateTempStoreSet() {
$metadata1 = $this->tempStore->getMetadata('foo');
$this->assertEquals('bar', $this->tempStore->get('foo'));
$this->assertNotEmpty($metadata1->owner);
$this->assertNotEmpty($metadata1->getOwnerId());
$this->tempStore->set('foo', 'bar2');
$metadata2 = $this->tempStore->getMetadata('foo');
$this->assertEquals('bar2', $this->tempStore->get('foo'));
$this->assertNotEmpty($metadata2->owner);
$this->assertEquals($metadata2->owner, $metadata1->owner);
$this->assertNotEmpty($metadata2->getOwnerId());
$this->assertEquals($metadata2->getOwnerId(), $metadata1->getOwnerId());
}
}
......@@ -75,6 +75,7 @@ public function testSharedTempStore() {
// fatal exception, because in that situation garbage collection is not
// triggered until the test class itself is destructed, after tearDown()
// has deleted the database tables. Store the objects locally instead.
/** @var \Drupal\Core\TempStore\SharedTempStore[] $stores */
$stores[$i] = $factory->get($collection, $users[$i]);
}
......@@ -85,11 +86,11 @@ public function testSharedTempStore() {
// FALSE the second time (when $i is 1).
$this->assertEqual(!$i, $stores[0]->setIfNotExists($key, $this->objects[$i]));
$metadata = $stores[0]->getMetadata($key);
$this->assertEqual($users[0], $metadata->owner);
$this->assertEqual($users[0], $metadata->getOwnerId());
$this->assertIdenticalObject($this->objects[0], $stores[0]->get($key));
// Another user should get the same result.
$metadata = $stores[1]->getMetadata($key);
$this->assertEqual($users[0], $metadata->owner);
$this->assertEqual($users[0], $metadata->getOwnerId());
$this->assertIdenticalObject($this->objects[0], $stores[1]->get($key));
}
......@@ -115,11 +116,11 @@ public function testSharedTempStore() {
$this->assertIdenticalObject($this->objects[3], $stores[0]->get($key));
$this->assertIdenticalObject($this->objects[3], $stores[1]->get($key));
$metadata = $stores[1]->getMetadata($key);
$this->assertEqual($users[1], $metadata->owner);
$this->assertEqual($users[1], $metadata->getOwnerId());
// The first user should be informed that the second now owns the data.
$metadata = $stores[0]->getMetadata($key);
$this->assertEqual($users[1], $metadata->owner);
$this->assertEqual($users[1], $metadata->getOwnerId());
// The first user should no longer be allowed to get, update, delete.
$this->assertNull($stores[0]->getIfOwner($key));
......
......@@ -2,6 +2,7 @@
namespace Drupal\Tests\Core\TempStore;
use Drupal\Core\TempStore\Lock;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\TempStore\TempStoreException;
......@@ -182,13 +183,45 @@ public function testGetMetadata() {
->will($this->returnValue(FALSE));
$metadata = $this->tempStore->getMetadata('test');
$this->assertObjectHasAttribute('owner', $metadata);
$this->assertInstanceOf(Lock::class, $metadata);
$this->assertObjectHasAttribute('ownerId', $metadata);
$this->assertObjectHasAttribute('updated', $metadata);
// Data should get removed.
$this->assertObjectNotHasAttribute('data', $metadata);
$this->assertNull($this->tempStore->getMetadata('test'));
}
/**
* @covers ::getMetadata
* @expectedDeprecation Using the "owner" public property of a TempStore lock is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\Core\TempStore\Lock::getOwnerId() instead. See https://www.drupal.org/node/3025869.
* @group legacy
*/
public function testGetMetadataOwner() {
$this->keyValue->expects($this->once())
->method('get')
->with('1:test')
->will($this->returnValue($this->ownObject));
$metadata = $this->tempStore->getMetadata('test');
$this->assertSame(1, $metadata->owner);
}
/**
* @covers ::getMetadata
* @expectedDeprecation Using the "updated" public property of a TempStore lock is deprecated in Drupal 8.7.0 and will not be allowed in Drupal 9.0.0. Use \Drupal\Core\TempStore\Lock::getUpdated() instead. See https://www.drupal.org/node/3025869.
* @group legacy
*/
public function testGetMetadataUpdated() {
$this->keyValue->expects($this->once())
->method('get')
->with('1:test')
->will($this->returnValue($this->ownObject));
$metadata = $this->tempStore->getMetadata('test');
$this->assertSame($metadata->getUpdated(), $metadata->updated);
}