Commit 37586d97 authored by webchick's avatar webchick

Issue #1837388 by tim.plunkett, dawehner, Wim Leers: Provide a ParamConverter...

Issue #1837388 by tim.plunkett, dawehner, Wim Leers: Provide a ParamConverter than can upcast any entity.
parent 4fb0f9d8
......@@ -124,6 +124,8 @@ public function getAccessControlHandler($entity_type);
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* A storage instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function getStorage($entity_type);
......
......@@ -64,8 +64,18 @@ public function __construct(EntityManagerInterface $entity_manager, ConfigFactor
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults, Request $request) {
$entity_type = substr($definition['type'], strlen('entity:'));
if ($storage = $this->entityManager->getStorage($entity_type)) {
$entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
// If the entity type is dynamic, confirm it to be a config entity. Static
// entity types will have performed this check in self::applies().
if (strpos($definition['type'], 'entity:{') === 0) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
if (!$entity_type->isSubclassOf('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
return parent::convert($value, $definition, $name, $defaults, $request);
}
}
if ($storage = $this->entityManager->getStorage($entity_type_id)) {
// Make sure no overrides are loaded.
$old_state = $this->configFactory->getOverrideState();
$this->configFactory->setOverrideState(FALSE);
......@@ -80,9 +90,13 @@ public function convert($value, $definition, $name, array $defaults, Request $re
*/
public function applies($definition, $name, Route $route) {
if (parent::applies($definition, $name, $route)) {
$entity_type_id = substr($definition['type'], strlen('entity:'));
// If the entity type is dynamic, defer checking to self::convert().
if (strpos($entity_type_id, '{') === 0) {
return TRUE;
}
// As we only want to override EntityConverter for ConfigEntities, find
// out whether the current entity is a ConfigEntity.
$entity_type_id = substr($definition['type'], strlen('entity:'));
$entity_type = $this->entityManager->getDefinition($entity_type_id);
if ($entity_type->isSubclassOf('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
return $this->adminContext->isAdminRoute($route);
......
......@@ -12,7 +12,33 @@
use Symfony\Component\Routing\Route;
/**
* Parameter converter for upcasting entity ids to full objects.
* Parameter converter for upcasting entity IDs to full objects.
*
* This is useful in cases where the dynamic elements of the path can't be
* auto-determined; for example, if your path refers to multiple of the same
* type of entity ("example/{node1}/foo/{node2}") or if the path can act on any
* entity type ("example/{entity_type}/{entity}/foo").
*
* In order to use it you should specify some additional options in your route:
* @code
* example.route:
* path: foo/{example}
* options:
* parameters:
* example:
* type: entity:node
* @endcode
*
* If you want to have the entity type itself dynamic in the url you can
* specify it like the following:
* @code
* example.route:
* path: foo/{entity_type}/{example}
* options:
* parameters:
* example:
* type: entity:{entity_type}
* @endcode
*/
class EntityConverter implements ParamConverterInterface {
......@@ -26,7 +52,7 @@ class EntityConverter implements ParamConverterInterface {
/**
* Constructs a new EntityConverter.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
......@@ -37,8 +63,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults, Request $request) {
$entity_type = substr($definition['type'], strlen('entity:'));
if ($storage = $this->entityManager->getStorage($entity_type)) {
$entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
if ($storage = $this->entityManager->getStorage($entity_type_id)) {
return $storage->load($value);
}
}
......@@ -48,10 +74,44 @@ public function convert($value, $definition, $name, array $defaults, Request $re
*/
public function applies($definition, $name, Route $route) {
if (!empty($definition['type']) && strpos($definition['type'], 'entity:') === 0) {
$entity_type = substr($definition['type'], strlen('entity:'));
return $this->entityManager->hasDefinition($entity_type);
$entity_type_id = substr($definition['type'], strlen('entity:'));
if (strpos($definition['type'], '{') !== FALSE) {
$entity_type_slug = substr($entity_type_id, 1, -1);
return $name != $entity_type_slug && in_array($entity_type_slug, $route->compile()->getVariables(), TRUE);
}
return $this->entityManager->hasDefinition($entity_type_id);
}
return FALSE;
}
/**
* Determines the entity type ID given a route definition and route defaults.
*
* @param mixed $definition
* The parameter definition provided in the route options.
* @param string $name
* The name of the parameter.
* @param array $defaults
* The route defaults array.
*
* @throws \Drupal\Core\ParamConverter\ParamNotConvertedException
* Thrown when the dynamic entity type is not found in the route defaults.
*
* @return string
* The entity type ID.
*/
protected function getEntityTypeFromDefaults($definition, $name, array $defaults) {
$entity_type_id = substr($definition['type'], strlen('entity:'));
// If the entity type is dynamic, it will be pulled from the route defaults.
if (strpos($entity_type_id, '{') === 0) {
$entity_type_slug = substr($entity_type_id, 1, -1);
if (!isset($defaults[$entity_type_slug])) {
throw new ParamNotConvertedException(sprintf('The "%s" parameter was not converted because the "%s" parameter is missing', $name, $entity_type_slug));
}
$entity_type_id = $defaults[$entity_type_slug];
}
return $entity_type_id;
}
}
......@@ -11,6 +11,9 @@ editor.field_untransformed_text:
_controller: '\Drupal\editor\EditorController::getUntransformedText'
options:
_theme: ajax_base_page
parameters:
entity:
type: entity:{entity_type}
requirements:
_permission: 'access in-place editing'
_access_quickedit_entity_field: 'TRUE'
......
......@@ -21,6 +21,9 @@ quickedit.field_form:
options:
_access_mode: 'ALL'
_theme: ajax_base_page
parameters:
entity:
type: entity:{entity_type}
requirements:
_permission: 'access in-place editing'
_access_quickedit_entity_field: 'TRUE'
......@@ -31,4 +34,8 @@ quickedit.entity_save:
_controller: '\Drupal\quickedit\QuickEditController::entitySave'
requirements:
_permission: 'access in-place editing'
_access_quickedit_entity: 'TRUE'
_entity_access: 'entity.update'
options:
parameters:
entity:
type: entity:{entity_type}
......@@ -4,14 +4,8 @@ services:
parent: default_plugin_manager
access_check.quickedit.entity_field:
class: Drupal\quickedit\Access\EditEntityFieldAccessCheck
arguments: ['@entity.manager']
tags:
- { name: access_check, applies_to: _access_quickedit_entity_field }
access_check.quickedit.entity:
class: Drupal\quickedit\Access\EditEntityAccessCheck
arguments: ['@entity.manager']
tags:
- { name: access_check, applies_to: _access_quickedit_entity }
quickedit.editor.selector:
class: Drupal\quickedit\EditorSelector
arguments: ['@plugin.manager.quickedit.editor', '@plugin.manager.field.formatter']
......
<?php
/**
* @file
* Contains \Drupal\quickedit\Access\EditEntityAccessCheck.
*/
namespace Drupal\quickedit\Access;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Entity\EntityInterface;
/**
* Access check for editing entities with QuickEdit.
*/
class EditEntityAccessCheck implements AccessInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a EditEntityAccessCheck object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* Checks Quick Edit access to the entity.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*
* @todo Replace $request parameter with $entity once
* https://drupal.org/node/1837388 is fixed.
*/
public function access(Request $request, AccountInterface $account) {
if (!$this->validateAndUpcastRequestAttributes($request)) {
return static::KILL;
}
return $this->accessEditEntity($request->attributes->get('entity'), $account) ? static::ALLOW : static::DENY;
}
/**
* {@inheritdoc}
*/
protected function accessEditEntity(EntityInterface $entity, $account) {
return $entity->access('update', $account);
}
/**
* Validates and upcasts request attributes.
*
* @todo Remove once https://drupal.org/node/1837388 is fixed.
*/
protected function validateAndUpcastRequestAttributes(Request $request) {
// Load the entity.
if (!is_object($entity = $request->attributes->get('entity'))) {
$entity_id = $entity;
$entity_type = $request->attributes->get('entity_type');
if (!$entity_type || !$this->entityManager->getDefinition($entity_type)) {
return FALSE;
}
$entity = $this->entityManager->getStorage($entity_type)->load($entity_id);
if (!$entity) {
return FALSE;
}
$request->attributes->set('entity', $entity);
}
return TRUE;
}
}
......@@ -7,10 +7,8 @@
namespace Drupal\quickedit\Access;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Entity\EntityInterface;
/**
......@@ -18,47 +16,29 @@
*/
class EditEntityFieldAccessCheck implements AccessInterface, EditEntityFieldAccessCheckInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a EditEntityFieldAccessCheck object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* Checks Quick Edit access to the field.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param string $field_name.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity containing the field.
* @param string $field_name
* The field name.
* @param string $langcode
* The langcode.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*
* @todo Replace $request parameter with $entity once
* https://drupal.org/node/1837388 is fixed.
*
* @todo Use the $account argument: https://drupal.org/node/2266809.
*/
public function access(Request $request, $field_name, AccountInterface $account) {
if (!$this->validateAndUpcastRequestAttributes($request)) {
public function access(EntityInterface $entity, $field_name, $langcode, AccountInterface $account) {
if (!$this->validateRequestAttributes($entity, $field_name, $langcode)) {
return static::KILL;
}
return $this->accessEditEntityField($request->attributes->get('entity'), $field_name) ? static::ALLOW : static::DENY;
return $this->accessEditEntityField($entity, $field_name) ? static::ALLOW : static::DENY;
}
/**
......@@ -69,31 +49,13 @@ public function accessEditEntityField(EntityInterface $entity, $field_name) {
}
/**
* Validates and upcasts request attributes.
*
* @todo Remove once https://drupal.org/node/1837388 is fixed.
* Validates request attributes.
*/
protected function validateAndUpcastRequestAttributes(Request $request) {
// Load the entity.
if (!is_object($entity = $request->attributes->get('entity'))) {
$entity_id = $entity;
$entity_type = $request->attributes->get('entity_type');
if (!$entity_type || !$this->entityManager->getDefinition($entity_type)) {
return FALSE;
}
$entity = $this->entityManager->getStorage($entity_type)->load($entity_id);
if (!$entity) {
return FALSE;
}
$request->attributes->set('entity', $entity);
}
protected function validateRequestAttributes(EntityInterface $entity, $field_name, $langcode) {
// Validate the field name and language.
$field_name = $request->attributes->get('field_name');
if (!$field_name || !$entity->hasField($field_name)) {
return FALSE;
}
$langcode = $request->attributes->get('langcode');
if (!$langcode || !$entity->hasTranslation($langcode)) {
return FALSE;
}
......
<?php
/**
* @file
* Contains \Drupal\quickedit\Tests\Access\EditEntityAccessCheckTest.
*/
namespace Drupal\quickedit\Tests\Access;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\quickedit\Access\EditEntityAccessCheck;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Entity\EntityInterface;
/**
* @coversDefaultClass \Drupal\quickedit\Access\EditEntityAccessCheck
* @group quickedit
*/
class EditEntityAccessCheckTest extends UnitTestCase {
/**
* The tested access checker.
*
* @var \Drupal\quickedit\Access\EditEntityAccessCheck
*/
protected $editAccessCheck;
/**
* The mocked entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityManager;
/**
* The mocked entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityStorage;
protected function setUp() {
$this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$this->entityStorage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
$this->entityManager->expects($this->any())
->method('getStorage')
->will($this->returnValue($this->entityStorage));
$this->editAccessCheck = new EditEntityAccessCheck($this->entityManager);
}
/**
* Provides test data for testAccess().
*
* @see \Drupal\quickedit\Tests\quickedit\Access\EditEntityAccessCheckTest::testAccess()
*/
public function providerTestAccess() {
$editable_entity = $this->getMockBuilder('Drupal\entity_test\Entity\EntityTest')
->disableOriginalConstructor()
->getMock();
$editable_entity->expects($this->any())
->method('access')
->will($this->returnValue(TRUE));
$non_editable_entity = $this->getMockBuilder('Drupal\entity_test\Entity\EntityTest')
->disableOriginalConstructor()
->getMock();
$non_editable_entity->expects($this->any())
->method('access')
->will($this->returnValue(FALSE));
$data = array();
$data[] = array($editable_entity, AccessCheckInterface::ALLOW);
$data[] = array($non_editable_entity, AccessCheckInterface::DENY);
return $data;
}
/**
* Tests the method for checking access to routes.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A mocked entity.
* @param bool|null $expected_result
* The expected result of the access call.
*
* @dataProvider providerTestAccess
*/
public function testAccess(EntityInterface $entity, $expected_result) {
$request = new Request();
// Prepare the request to be valid.
$request->attributes->set('entity', $entity);
$request->attributes->set('entity_type', 'test_entity');
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$access = $this->editAccessCheck->access($request, $account);
$this->assertSame($expected_result, $access);
}
/**
* Tests the access method with an undefined entity type.
*/
public function testAccessWithUndefinedEntityType() {
$request = new Request();
$request->attributes->set('entity_type', 'non_valid');
$this->entityManager->expects($this->once())
->method('getDefinition')
->with('non_valid')
->will($this->returnValue(NULL));
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, $account));
}
/**
* Tests the access method with a non existing entity.
*/
public function testAccessWithNotExistingEntity() {
$request = new Request();
$request->attributes->set('entity_type', 'entity_test');
$request->attributes->set('entity', 1);
$this->entityManager->expects($this->once())
->method('getDefinition')
->with('entity_test')
->will($this->returnValue(array('id' => 'entity_test')));
$this->entityStorage->expects($this->once())
->method('load')
->with(1)
->will($this->returnValue(NULL));
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, $account));
}
}
......@@ -7,7 +7,6 @@
namespace Drupal\quickedit\Tests\Access;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\quickedit\Access\EditEntityFieldAccessCheck;
use Drupal\Tests\UnitTestCase;
......@@ -29,29 +28,10 @@ class EditEntityFieldAccessCheckTest extends UnitTestCase {
protected $editAccessCheck;
/**
* The mocked entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
* {@inheritdoc}
*/
protected $entityManager;
/**
* The mocked entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityStorage;
protected function setUp() {
$this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$this->entityStorage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
$this->entityManager->expects($this->any())
->method('getStorage')
->will($this->returnValue($this->entityStorage));
$this->editAccessCheck = new EditEntityFieldAccessCheck($this->entityManager);
$this->editAccessCheck = new EditEntityFieldAccessCheck();
}
/**
......@@ -105,8 +85,6 @@ public function providerTestAccess() {
* @dataProvider providerTestAccess
*/
public function testAccess(EntityInterface $entity, FieldStorageConfigInterface $field_storage = NULL, $expected_result) {
$request = new Request();
$field_name = 'valid';
$entity_with_field = clone $entity;
$entity_with_field->expects($this->any())
......@@ -118,124 +96,54 @@ public function testAccess(EntityInterface $entity, FieldStorageConfigInterface
->with(LanguageInterface::LANGCODE_NOT_SPECIFIED)
->will($this->returnValue(TRUE));
// Prepare the request to be valid.
$request->attributes->set('entity_type', 'test_entity');
$request->attributes->set('entity', $entity_with_field);
$request->attributes->set('field_name', $field_name);
$request->attributes->set('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$access = $this->editAccessCheck->access($request, $field_name, $account);
$access = $this->editAccessCheck->access($entity_with_field, $field_name, LanguageInterface::LANGCODE_NOT_SPECIFIED, $account);
$this->assertSame($expected_result, $access);
}
/**
* Tests the access method with an undefined entity type.
*/
public function testAccessWithUndefinedEntityType() {
$request = new Request();
$request->attributes->set('entity_type', 'non_valid');
$this->entityManager->expects($this->once())
->method('getDefinition')
->with('non_valid')
->will($this->returnValue(NULL));
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, NULL, $account));
}
/**
* Tests the access method with a non existing entity.
*/
public function testAccessWithNotExistingEntity() {
$request = new Request();
$request->attributes->set('entity_type', 'entity_test');
$request->attributes->set('entity', 1);
$this->entityManager->expects($this->once())
->method('getDefinition')
->with('entity_test')
->will($this->returnValue(array('id' => 'entity_test')));
$this->entityStorage->expects($this->once())
->method('load')
->with(1)
->will($this->returnValue(NULL));
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, NULL, $account));
}
/**
* Tests the access method with a forgotten passed field_name.
*/
public function testAccessWithNotPassedFieldName() {
$request = new Request();
$request->attributes->set('entity_type', 'entity_test');
$request->attributes->set('entity', $this->createMockEntity());
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, NULL, $account));
}
/**
* Tests the access method with a non existing field.
*/
public function testAccessWithNonExistingField() {
$request = new Request();
$field_name = 'not_valid';
$request->attributes->set('entity_type', 'entity_test');
$request->attributes->set('entity', $this->createMockEntity());
$request->attributes->set('field_name', $field_name);
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, $field_name, $account));
}
/**
* Tests the access method with a forgotten passed language.
* Tests checking access to routes that result in AccessCheckInterface::KILL.
*
* @dataProvider providerTestAccessKill
*/
public function testAccessWithNotPassedLanguage() {
$request = new Request();
$field_name = 'valid';
$request->attributes->set('entity_type', 'entity_test');
$request->attributes->set('entity', $this->createMockEntity());
$request->attributes->set('field_name', $field_name);
public function testAccessKill($field_name, $langcode) {
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, $field_name, $account));
$entity = $this->createMockEntity();
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($entity, $field_name, $langcode, $account));
}
/**
* Tests the access method with an invalid language.
* Provides test data for testAccessKill.
*/
public function testAccessWithInvalidLanguage() {
$entity = $this->createMockEntity();
$entity->expects($this->once())
->method('hasTranslation')
->with('xx-lolspeak')
->will($this->returnValue(FALSE));
$request = new Request();
$field_name = 'valid';
$request->attributes->set('entity_type', 'entity_test');
$request->attributes->set('entity', $entity);
$request->attributes->set('field_name', $field_name);
$request->attributes->set('langcode', 'xx-lolspeak');
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertSame(AccessCheckInterface::KILL, $this->editAccessCheck->access($request, $field_name, $account));
public function providerTestAccessKill() {