Commit 6a16932c authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2724823 by tedbow, dawehner, Wim Leers, clemens.tolboom:...

Issue #2724823 by tedbow, dawehner, Wim Leers, clemens.tolboom: EntityResource: read-only (GET) support for configuration entities
parent 671a41e6
Loading
Loading
Loading
Loading
+79 −0
Changes for core/modules/rest/src/Plugin/rest/resource/EntityResource.php: 79 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2,11 +2,16 @@

namespace Drupal\rest\Plugin\rest\resource;

use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\rest\ModifiedResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -29,6 +34,48 @@
 */
class EntityResource extends ResourceBase {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object.
   *
   * @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 array $serializer_formats
   *   The available serialization formats.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->getParameter('serializer.formats'),
      $container->get('logger.factory')->get('rest'),
      $container->get('entity_type.manager')
    );
  }

  /**
   * Responds to entity GET requests.
   *
@@ -49,6 +96,8 @@ public function get(EntityInterface $entity) {
    $response = new ResourceResponse($entity, 200);
    $response->addCacheableDependency($entity);
    $response->addCacheableDependency($entity_access);

    if ($entity instanceof FieldableEntityInterface) {
      foreach ($entity as $field_name => $field) {
        /** @var \Drupal\Core\Field\FieldItemListInterface $field */
        $field_access = $field->access('view', NULL, TRUE);
@@ -58,6 +107,7 @@ public function get(EntityInterface $entity) {
          $entity->set($field_name, NULL);
        }
      }
    }

    return $response;
  }
@@ -223,6 +273,10 @@ public function delete(EntityInterface $entity) {
   *   If validation errors are found.
   */
  protected function validate(EntityInterface $entity) {
    // @todo Remove when https://www.drupal.org/node/2164373 is committed.
    if (!$entity instanceof FieldableEntityInterface) {
      return;
    }
    $violations = $entity->validate();

    // Remove violations of inaccessible fields as they cannot stem from our
@@ -256,4 +310,29 @@ protected function getBaseRoute($canonical_path, $method) {
    return $route;
  }

  /**
   * {@inheritdoc}
   */
  public function availableMethods() {
    $methods = parent::availableMethods();
    if ($this->isConfigEntityResource()) {
      // Currently only GET is supported for Config Entities.
      // @todo Remove when supported https://www.drupal.org/node/2300677
      $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH'];
      $methods = array_diff($methods, $unsupported_methods);
    }
    return $methods;
  }

  /**
   * Checks if this resource is for a Config Entity.
   *
   * @return bool
   *   TRUE if the entity is a Config Entity, FALSE otherwise.
   */
  protected function isConfigEntityResource() {
    $entity_type_id = $this->getPluginDefinition()['entity_type'];
    return $this->entityTypeManager->getDefinition($entity_type_id) instanceof ConfigEntityType;
  }

}
+1 −0
Changes for core/modules/rest/src/RequestHandler.php: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
      ->createInstance($plugin);

    // Deserialize incoming data if available.
    /** @var \Symfony\Component\Serializer\SerializerInterface $serializer */
    $serializer = $this->container->get('serializer');
    $received = $request->getContent();
    $unserialized = NULL;
+74 −8
Changes for core/modules/rest/src/Tests/RESTTestBase.php: 74 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\rest\Tests;

use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\node\NodeInterface;
use Drupal\simpletest\WebTestBase;

@@ -200,14 +201,14 @@ protected function entityCreate($entity_type) {
   * Required properties differ from entity type to entity type, so we keep a
   * minimum mapping here.
   *
   * @param string $entity_type
   *   The type of the entity that should be created.
   * @param string $entity_type_id
   *   The ID of the type of entity that should be created.
   *
   * @return array
   *   An array of values keyed by property name.
   */
  protected function entityValues($entity_type) {
    switch ($entity_type) {
  protected function entityValues($entity_type_id) {
    switch ($entity_type_id) {
      case 'entity_test':
        return array(
          'name' => $this->randomMachineName(),
@@ -217,6 +218,11 @@ protected function entityValues($entity_type) {
            'format' => 'plain_text',
          )),
        );
      case 'config_test':
        return [
          'id' => $this->randomMachineName(),
          'label' => 'Test label',
        ];
      case 'node':
        return array('title' => $this->randomString(), 'type' => 'resttest');
      case 'node_type':
@@ -236,8 +242,15 @@ protected function entityValues($entity_type) {
          'entity_id' => 'invalid',
          'field_name' => 'comment',
        ];

      case 'taxonomy_vocabulary':
        return [
          'vid' => 'tags',
          'name' => $this->randomMachineName(),
        ];
      default:
        if ($this->isConfigEntity($entity_type_id)) {
          return $this->configEntityValues($entity_type_id);
        }
        return array();
    }
  }
@@ -311,7 +324,7 @@ protected function curlExec($curl_options, $redirect = FALSE) {
  /**
   * Provides the necessary user permissions for entity operations.
   *
   * @param string $entity_type
   * @param string $entity_type_id
   *   The entity type.
   * @param string $operation
   *   The operation, one of 'view', 'create', 'update' or 'delete'.
@@ -319,8 +332,8 @@ protected function curlExec($curl_options, $redirect = FALSE) {
   * @return array
   *   The set of user permission strings.
   */
  protected function entityPermissions($entity_type, $operation) {
    switch ($entity_type) {
  protected function entityPermissions($entity_type_id, $operation) {
    switch ($entity_type_id) {
      case 'entity_test':
        switch ($operation) {
          case 'view':
@@ -365,9 +378,17 @@ protected function entityPermissions($entity_type, $operation) {

          default:
            return ['administer users'];
        }

      default:
        if ($this->isConfigEntity($entity_type_id)) {
          $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
          if ($admin_permission = $entity_type->getAdminPermission()) {
            return [$admin_permission];
          }
        }
    }
    return [];
  }

  /**
@@ -431,4 +452,49 @@ protected function assertResponseBody($expected, $message = '', $group = 'REST R
    return $this->assertIdentical($expected, $this->responseBody, $message ? $message : strtr('Response body @expected (expected) is equal to @response (actual).', array('@expected' => var_export($expected, TRUE), '@response' => var_export($this->responseBody, TRUE))), $group);
  }

  /**
   * Checks if an entity type id is for a Config Entity.
   *
   * @param string $entity_type_id
   *   The entity type ID to check.
   *
   * @return bool
   *   TRUE if the entity is a Config Entity, FALSE otherwise.
   */
  protected function isConfigEntity($entity_type_id) {
    return \Drupal::entityTypeManager()->getDefinition($entity_type_id) instanceof ConfigEntityType;
  }

  /**
   * Provides an array of suitable property values for a config entity type.
   *
   * Config entities have some common keys that need to be created. Required
   * properties differ among config entity types, so we keep a minimum mapping
   * here.
   *
   * @param string $entity_type_id
   *   The ID of the type of entity that should be created.
   *
   * @return array
   *   An array of values keyed by property name.
   */
  protected function configEntityValues($entity_type_id) {
    $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
    $keys = $entity_type->getKeys();
    $values = [];
    // Fill out known key values that are shared across entity types.
    foreach ($keys as $key) {
      if ($key === 'id' || $key === 'label') {
        $values[$key] = $this->randomMachineName();
      }
    }
    // Add extra values for particular entity types.
    switch ($entity_type_id) {
      case 'block':
        $values['plugin'] = 'system_powered_by_block';
        break;
    }
    return $values;
  }

}
+86 −11
Changes for core/modules/rest/src/Tests/ReadTest.php: 86 added lines, 11 removed lines.
Original line number Diff line number Diff line
@@ -3,6 +3,8 @@
namespace Drupal\rest\Tests;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;

/**
@@ -17,7 +19,14 @@ class ReadTest extends RESTTestBase {
   *
   * @var array
   */
  public static $modules = array('hal', 'rest', 'entity_test');
  public static $modules = [
    'hal',
    'rest',
    'entity_test',
    'config_test',
    'taxonomy',
    'block',
  ];

  /**
   * Tests several valid and invalid read requests on all entity types.
@@ -25,7 +34,14 @@ class ReadTest extends RESTTestBase {
  public function testRead() {
    // @todo Expand this at least to users.
    // Define the entity types we want to test.
    $entity_types = array('entity_test', 'node');
    $entity_types = [
      'entity_test',
      'node',
      'config_test',
      'taxonomy_vocabulary',
      'block',
      'user_role',
    ];
    foreach ($entity_types as $entity_type) {
      $this->enableService('entity:' . $entity_type, 'GET');
      // Create a user account that has the required permissions to read
@@ -39,23 +55,39 @@ public function testRead() {
      $entity = $this->entityCreate($entity_type);
      $entity->save();
      // Read it over the REST API.
      $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
      $response = $this->httpRequest($this->getReadUrl($entity), 'GET');
      $this->assertResponse('200', 'HTTP response code is correct.');
      $this->assertHeader('content-type', $this->defaultMimeType);
      $data = Json::decode($response);
      // Only assert one example property here, other properties should be
      // checked in serialization tests.
      if ($entity instanceof ConfigEntityInterface) {
        $this->assertEqual($data['uuid'], $entity->uuid(), 'Entity UUID is correct');
      }
      else {
        $this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
      }

      // Try to read the entity with an unsupported mime format.
      $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'wrongformat'), 'GET');
      $this->httpRequest($this->getReadUrl($entity, 'wrongformat'), 'GET');
      $this->assertResponse(406);
      $this->assertHeader('Content-type', 'application/json');

      // Try to read an entity that does not exist.
      $response = $this->httpRequest(Url::fromUri('base://' . $entity_type . '/9999', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
      $response = $this->httpRequest($this->getReadUrl($entity, $this->defaultFormat, 9999), 'GET');
      $this->assertResponse(404);
      $path = $entity_type == 'node' ? '/node/{node}' : '/entity_test/{entity_test}';
      switch ($entity_type) {
        case 'node':
          $path = '/node/{node}';
          break;

        case 'entity_test':
          $path = '/entity_test/{entity_test}';
          break;

        default:
          $path = "/entity/$entity_type/{" . $entity_type . '}';
      }
      $expected_message = Json::encode(['message' => 'The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']);
      $this->assertIdentical($expected_message, $response, 'Response message is correct.');

@@ -65,7 +97,7 @@ public function testRead() {
      if ($entity_type == 'entity_test') {
        $entity->field_test_text->value = 'no access value';
        $entity->save();
        $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
        $response = $this->httpRequest($this->getReadUrl($entity), 'GET');
        $this->assertResponse(200);
        $this->assertHeader('content-type', $this->defaultMimeType);
        $data = Json::decode($response);
@@ -74,14 +106,15 @@ public function testRead() {

      // Try to read an entity without proper permissions.
      $this->drupalLogout();
      $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
      $response = $this->httpRequest($this->getReadUrl($entity), 'GET');
      $this->assertResponse(403);
      $this->assertIdentical('{"message":""}', $response);
    }
    // Try to read a resource which is not REST API enabled.
    // Try to read a resource, the user entity, which is not REST API enabled.
    $account = $this->drupalCreateUser();
    $this->drupalLogin($account);
    $response = $this->httpRequest($account->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
    $response = $this->httpRequest($this->getReadUrl($account), 'GET');

    // \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
    // non-REST route a match, but a lower quality one: no format restrictions
    // means there's always a match and hence when there is no matching REST
@@ -111,8 +144,50 @@ public function testResourceStructure() {
    $entity->save();

    // Read it over the REST API.
    $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'json'), 'GET');
    $this->httpRequest($this->getReadUrl($entity, 'json'), 'GET');
    $this->assertResponse('200', 'HTTP response code is correct.');
  }

  /**
   * Gets the read URL object for the entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to get the URL for.
   * @param string $format
   *   The format to request the entity in.
   * @param string $entity_id
   *   The entity ID to use in the URL, defaults to the entity's ID if know
   *   given.
   *
   * @return \Drupal\Core\Url
   *   The Url object.
   */
  protected function getReadUrl(EntityInterface $entity, $format = NULL, $entity_id = NULL) {
    if (!$format) {
      $format = $this->defaultFormat;
    }
    if (!$entity_id) {
      $entity_id = $entity->id();
    }
    $entity_type = $entity->getEntityTypeId();
    if ($entity->hasLinkTemplate('canonical')) {
      $url = $entity->toUrl('canonical');
    }
    else {
      $route_name = 'rest.entity.' . $entity_type . ".GET.";
      // If testing unsupported format don't use the format to construct route
      // name. This would give a RouteNotFoundException.
      if ($format == 'wrongformat') {
        $route_name .= $this->defaultFormat;
      }
      else {
        $route_name .= $format;
      }
      $url = Url::fromRoute($route_name);
    }
    $url->setRouteParameter($entity_type, $entity_id);
    $url->setRouteParameter('_format', $format);
    return $url;
  }

}