EntityResource.php 7.71 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Definition of Drupal\rest\Plugin\rest\resource\EntityResource.
 */

namespace Drupal\rest\Plugin\rest\resource;

10
use Drupal\Core\Entity\EntityInterface;
11 12
use Drupal\Core\Entity\EntityStorageException;
use Drupal\rest\Plugin\ResourceBase;
13
use Drupal\rest\ResourceResponse;
14
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
15
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
16 17 18 19 20 21
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Represents entities as resources.
 *
22
 * @RestResource(
23 24
 *   id = "entity",
 *   label = @Translation("Entity"),
25
 *   serialization_class = "Drupal\Core\Entity\Entity",
26
 *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
27 28 29 30
 * )
 */
class EntityResource extends ResourceBase {

31 32 33 34 35 36 37 38 39 40 41 42
  /**
   * Responds to entity GET requests.
   *
   * @param mixed $id
   *   The entity ID.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The response containing the loaded entity.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   */
  public function get($id) {
43
    $definition = $this->getPluginDefinition();
44 45
    $entity = entity_load($definition['entity_type'], $id);
    if ($entity) {
46 47 48 49 50 51 52 53
      if (!$entity->access('view')) {
        throw new AccessDeniedHttpException();
      }
      foreach ($entity as $field_name => $field) {
        if (!$field->access('view')) {
          unset($entity->{$field_name});
        }
      }
54 55 56 57 58
      return new ResourceResponse($entity);
    }
    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
  }

59 60 61 62 63 64 65 66 67 68 69 70 71
  /**
   * Responds to entity POST requests and saves the new entity.
   *
   * @param mixed $id
   *   Ignored. A new entity is created with a new ID.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The HTTP response object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   */
72 73 74 75 76
  public function post($id, EntityInterface $entity = NULL) {
    if ($entity == NULL) {
      throw new BadRequestHttpException(t('No entity content received.'));
    }

77 78 79
    if (!$entity->access('create')) {
      throw new AccessDeniedHttpException();
    }
80
    $definition = $this->getPluginDefinition();
81 82
    // Verify that the deserialized entity is of the type that we expect to
    // prevent security issues.
83
    if ($entity->getEntityTypeId() != $definition['entity_type']) {
84
      throw new BadRequestHttpException(t('Invalid entity type'));
85 86 87 88
    }
    // POSTed entities must not have an ID set, because we always want to create
    // new entities here.
    if (!$entity->isNew()) {
89
      throw new BadRequestHttpException(t('Only new entities can be created'));
90
    }
91 92 93 94 95
    foreach ($entity as $field_name => $field) {
      if (!$field->access('create')) {
        throw new AccessDeniedHttpException(t('Access denied on creating field @field.', array('@field' => $field_name)));
      }
    }
96 97 98

    // Validate the received data before saving.
    $this->validate($entity);
99 100
    try {
      $entity->save();
101
      watchdog('rest', 'Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
102

103
      $url = url(strtr($this->pluginId, ':', '/') . '/' . $entity->id(), array('absolute' => TRUE));
104 105 106 107
      // 201 Created responses have an empty body.
      return new ResourceResponse(NULL, 201, array('Location' => $url));
    }
    catch (EntityStorageException $e) {
108
      throw new HttpException(500, t('Internal Server Error'), $e);
109 110 111
    }
  }

112 113 114 115 116 117 118 119 120 121 122 123 124
  /**
   * Responds to entity PATCH requests.
   *
   * @param mixed $id
   *   The entity ID.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The HTTP response object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   */
125 126 127 128 129
  public function patch($id, EntityInterface $entity = NULL) {
    if ($entity == NULL) {
      throw new BadRequestHttpException(t('No entity content received.'));
    }

130 131 132
    if (empty($id)) {
      throw new NotFoundHttpException();
    }
133
    $definition = $this->getPluginDefinition();
134
    if ($entity->getEntityTypeId() != $definition['entity_type']) {
135
      throw new BadRequestHttpException(t('Invalid entity type'));
136 137 138 139 140 141 142
    }
    $original_entity = entity_load($definition['entity_type'], $id);
    // We don't support creating entities with PATCH, so we throw an error if
    // there is no existing entity.
    if ($original_entity == FALSE) {
      throw new NotFoundHttpException();
    }
143 144 145 146
    if (!$original_entity->access('update')) {
      throw new AccessDeniedHttpException();
    }

147
    // Overwrite the received properties.
148 149
    foreach ($entity as $field_name => $field) {
      if (isset($entity->{$field_name})) {
150 151
        if ($field->isEmpty() && !$original_entity->get($field_name)->access('delete')) {
          throw new AccessDeniedHttpException(t('Access denied on deleting field @field.', array('@field' => $field_name)));
152
        }
153
        $original_entity->set($field_name, $field->getValue());
154 155 156
        if (!$original_entity->get($field_name)->access('update')) {
          throw new AccessDeniedHttpException(t('Access denied on updating field @field.', array('@field' => $field_name)));
        }
157 158
      }
    }
159 160 161

    // Validate the received data before saving.
    $this->validate($original_entity);
162 163
    try {
      $original_entity->save();
164
      watchdog('rest', 'Updated entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
165

166 167 168 169
      // Update responses have an empty body.
      return new ResourceResponse(NULL, 204);
    }
    catch (EntityStorageException $e) {
170
      throw new HttpException(500, t('Internal Server Error'), $e);
171 172 173
    }
  }

174 175 176 177 178 179
  /**
   * Responds to entity DELETE requests.
   *
   * @param mixed $id
   *   The entity ID.
   *
180 181
   * @return \Drupal\rest\ResourceResponse
   *   The HTTP response object.
182 183 184 185
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   */
  public function delete($id) {
186
    $definition = $this->getPluginDefinition();
187 188
    $entity = entity_load($definition['entity_type'], $id);
    if ($entity) {
189 190 191
      if (!$entity->access('delete')) {
        throw new AccessDeniedHttpException();
      }
192 193
      try {
        $entity->delete();
194
        watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
195

196
        // Delete responses have an empty body.
197
        return new ResourceResponse(NULL, 204);
198 199
      }
      catch (EntityStorageException $e) {
200
        throw new HttpException(500, t('Internal Server Error'), $e);
201 202 203 204
      }
    }
    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
  }
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

  /**
   * Verifies that the whole entity does not violate any validation constraints.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   *   If validation errors are found.
   */
  protected function validate(EntityInterface $entity) {
    $violations = $entity->validate();
    if (count($violations) > 0) {
      $message = "Unprocessable Entity: validation failed.\n";
      foreach ($violations as $violation) {
        $message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n";
      }
      // Instead of returning a generic 400 response we use the more specific
      // 422 Unprocessable Entity code from RFC 4918. That way clients can
      // distinguish between general syntax errors in bad serializations (code
      // 400) and semantic errors in well-formed requests (code 422).
      throw new HttpException(422, $message);
    }
  }
229
}