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); ...@@ -124,6 +124,8 @@ public function getAccessControlHandler($entity_type);
* *
* @return \Drupal\Core\Entity\EntityStorageInterface * @return \Drupal\Core\Entity\EntityStorageInterface
* A storage instance. * A storage instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/ */
public function getStorage($entity_type); public function getStorage($entity_type);
......
...@@ -64,8 +64,18 @@ public function __construct(EntityManagerInterface $entity_manager, ConfigFactor ...@@ -64,8 +64,18 @@ public function __construct(EntityManagerInterface $entity_manager, ConfigFactor
* {@inheritdoc} * {@inheritdoc}
*/ */
public function convert($value, $definition, $name, array $defaults, Request $request) { public function convert($value, $definition, $name, array $defaults, Request $request) {
$entity_type = substr($definition['type'], strlen('entity:')); $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
if ($storage = $this->entityManager->getStorage($entity_type)) {
// 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. // Make sure no overrides are loaded.
$old_state = $this->configFactory->getOverrideState(); $old_state = $this->configFactory->getOverrideState();
$this->configFactory->setOverrideState(FALSE); $this->configFactory->setOverrideState(FALSE);
...@@ -80,9 +90,13 @@ public function convert($value, $definition, $name, array $defaults, Request $re ...@@ -80,9 +90,13 @@ public function convert($value, $definition, $name, array $defaults, Request $re
*/ */
public function applies($definition, $name, Route $route) { public function applies($definition, $name, Route $route) {
if (parent::applies($definition, $name, $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 // As we only want to override EntityConverter for ConfigEntities, find
// out whether the current entity is a ConfigEntity. // out whether the current entity is a ConfigEntity.
$entity_type_id = substr($definition['type'], strlen('entity:'));
$entity_type = $this->entityManager->getDefinition($entity_type_id); $entity_type = $this->entityManager->getDefinition($entity_type_id);
if ($entity_type->isSubclassOf('\Drupal\Core\Config\Entity\ConfigEntityInterface')) { if ($entity_type->isSubclassOf('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
return $this->adminContext->isAdminRoute($route); return $this->adminContext->isAdminRoute($route);
......
...@@ -12,7 +12,33 @@ ...@@ -12,7 +12,33 @@
use Symfony\Component\Routing\Route; 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 { class EntityConverter implements ParamConverterInterface {
...@@ -26,7 +52,7 @@ class EntityConverter implements ParamConverterInterface { ...@@ -26,7 +52,7 @@ class EntityConverter implements ParamConverterInterface {
/** /**
* Constructs a new EntityConverter. * Constructs a new EntityConverter.
* *
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager. * The entity manager.
*/ */
public function __construct(EntityManagerInterface $entity_manager) { public function __construct(EntityManagerInterface $entity_manager) {
...@@ -37,8 +63,8 @@ public function __construct(EntityManagerInterface $entity_manager) { ...@@ -37,8 +63,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function convert($value, $definition, $name, array $defaults, Request $request) { public function convert($value, $definition, $name, array $defaults, Request $request) {
$entity_type = substr($definition['type'], strlen('entity:')); $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
if ($storage = $this->entityManager->getStorage($entity_type)) { if ($storage = $this->entityManager->getStorage($entity_type_id)) {
return $storage->load($value); return $storage->load($value);
} }
} }
...@@ -48,10 +74,44 @@ public function convert($value, $definition, $name, array $defaults, Request $re ...@@ -48,10 +74,44 @@ public function convert($value, $definition, $name, array $defaults, Request $re
*/ */
public function applies($definition, $name, Route $route) { public function applies($definition, $name, Route $route) {
if (!empty($definition['type']) && strpos($definition['type'], 'entity:') === 0) { if (!empty($definition['type']) && strpos($definition['type'], 'entity:') === 0) {
$entity_type = substr($definition['type'], strlen('entity:')); $entity_type_id = substr($definition['type'], strlen('entity:'));
return $this->entityManager->hasDefinition($entity_type); 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; 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: ...@@ -11,6 +11,9 @@ editor.field_untransformed_text:
_controller: '\Drupal\editor\EditorController::getUntransformedText' _controller: '\Drupal\editor\EditorController::getUntransformedText'
options: options:
_theme: ajax_base_page _theme: ajax_base_page
parameters:
entity:
type: entity:{entity_type}
requirements: requirements:
_permission: 'access in-place editing' _permission: 'access in-place editing'
_access_quickedit_entity_field: 'TRUE' _access_quickedit_entity_field: 'TRUE'
......
...@@ -21,6 +21,9 @@ quickedit.field_form: ...@@ -21,6 +21,9 @@ quickedit.field_form:
options: options:
_access_mode: 'ALL' _access_mode: 'ALL'
_theme: ajax_base_page _theme: ajax_base_page
parameters:
entity:
type: entity:{entity_type}
requirements: requirements:
_permission: 'access in-place editing' _permission: 'access in-place editing'
_access_quickedit_entity_field: 'TRUE' _access_quickedit_entity_field: 'TRUE'
...@@ -31,4 +34,8 @@ quickedit.entity_save: ...@@ -31,4 +34,8 @@ quickedit.entity_save:
_controller: '\Drupal\quickedit\QuickEditController::entitySave' _controller: '\Drupal\quickedit\QuickEditController::entitySave'
requirements: requirements:
_permission: 'access in-place editing' _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: ...@@ -4,14 +4,8 @@ services:
parent: default_plugin_manager parent: default_plugin_manager
access_check.quickedit.entity_field: access_check.quickedit.entity_field:
class: Drupal\quickedit\Access\EditEntityFieldAccessCheck class: Drupal\quickedit\Access\EditEntityFieldAccessCheck
arguments: ['@entity.manager']
tags: tags:
- { name: access_check, applies_to: _access_quickedit_entity_field } - { 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: quickedit.editor.selector:
class: Drupal\quickedit\EditorSelector class: Drupal\quickedit\EditorSelector
arguments: ['@plugin.manager.quickedit.editor', '@plugin.manager.field.formatter'] 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 @@ ...@@ -7,10 +7,8 @@
namespace Drupal\quickedit\Access; namespace Drupal\quickedit\Access;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
/** /**
...@@ -18,47 +16,29 @@ ...@@ -18,47 +16,29 @@
*/ */
class EditEntityFieldAccessCheck implements AccessInterface, EditEntityFieldAccessCheckInterface { 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. * Checks Quick Edit access to the field.
* *
* @param \Symfony\Component\HttpFoundation\Request $request * @param \Drupal\Core\Entity\EntityInterface $entity
* The request object. * The entity containing the field.
* @param string $field_name. * @param string $field_name
* The field name. * The field name.
* @param string $langcode
* The langcode.
* @param \Drupal\Core\Session\AccountInterface $account * @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account. * The currently logged in account.
* *
* @return string * @return string
* A \Drupal\Core\Access\AccessInterface constant value. * 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. * @todo Use the $account argument: https://drupal.org/node/2266809.
*/ */
public function access(Request $request, $field_name, AccountInterface $account) { public function access(EntityInterface $entity, $field_name, $langcode, AccountInterface $account) {
if (!$this->validateAndUpcastRequestAttributes($request)) { if (!$this->validateRequestAttributes($entity, $field_name, $langcode)) {
return static::KILL; 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) { ...@@ -69,31 +49,13 @@ public function accessEditEntityField(EntityInterface $entity, $field_name) {
} }
/** /**
* Validates and upcasts request attributes. * Validates request attributes.
*
* @todo Remove once https://drupal.org/node/1837388 is fixed.
*/ */
protected function validateAndUpcastRequestAttributes(Request $request) { protected function validateRequestAttributes(EntityInterface $entity, $field_name, $langcode) {
// 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);
}
// Validate the field name and language. // Validate the field name and language.
$field_name = $request->attributes->get('field_name');
if (!$field_name || !$entity->hasField($field_name)) { if (!$field_name || !$entity->hasField($field_name)) {
return FALSE; return FALSE;
} }
$langcode = $request->attributes->get('langcode');
if (!$langcode || !$entity->hasTranslation($langcode)) { if (!$langcode || !$entity->hasTranslation($langcode)) {
return FALSE; 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 @@ ...@@ -7,7 +7,6 @@
namespace Drupal\quickedit\Tests\Access; namespace Drupal\quickedit\Tests\Access;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Access\AccessCheckInterface;