commit f7b649d9d9394d6d6bb999f1bd41b718f4e4fcb0 Author: Gabriel SULLICE Date: Thu Oct 31 15:19:06 2019 -0600 Address Matt's feedback and add extra error handling for decoding payloads diff --git a/jsonapi_resources.services.yml b/jsonapi_resources.services.yml index 1dd365e..05a04ce 100644 --- a/jsonapi_resources.services.yml +++ b/jsonapi_resources.services.yml @@ -14,6 +14,7 @@ services: jsonapi_resources.argument_resolver.document: class: Drupal\jsonapi_resources\Controller\ArgumentResolver\DocumentResolver + arguments: ['@jsonapi.serializer'] public: false jsonapi_resources.resource_response_factory: diff --git a/src/Controller/ArgumentResolver/DocumentResolver.php b/src/Controller/ArgumentResolver/DocumentResolver.php index 5919a80..3cb921c 100644 --- a/src/Controller/ArgumentResolver/DocumentResolver.php +++ b/src/Controller/ArgumentResolver/DocumentResolver.php @@ -2,18 +2,20 @@ namespace Drupal\jsonapi_resources\Controller\ArgumentResolver; -use Drupal\Component\Serialization\Json; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\JsonApiResource\LinkCollection; use Drupal\jsonapi\JsonApiResource\NullIncludedData; use Drupal\jsonapi\JsonApiResource\ResourceObjectData; +use Drupal\jsonapi\Serializer\Serializer; use Drupal\jsonapi_resources\Unstable\NewResourceObject; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** * Deserializes POST, PATCH and DELETE request documents. @@ -22,6 +24,23 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; */ final class DocumentResolver implements ArgumentValueResolverInterface { + /** + * The JSON:API serializer. + * + * @var \Drupal\jsonapi\Serializer\Serializer + */ + protected $serializer; + + /** + * Constructs a JSON:API document argument resolver. + * + * @param \Drupal\jsonapi\Serializer\Serializer $serializer + * The JSON:API serializer. + */ + public function __construct(Serializer $serializer) { + $this->serializer = $serializer; + } + /** * {@inheritdoc} */ @@ -36,7 +55,20 @@ final class DocumentResolver implements ArgumentValueResolverInterface { * {@inheritdoc} */ public function resolve(Request $request, ArgumentMetadata $argument) { - $decoded = Json::decode((string) $request->getContent()); + yield new JsonApiDocumentTopLevel(new ResourceObjectData([$this->extractResourceObjectFromRequest($request)], 1), new NullIncludedData(), new LinkCollection([])); + } + + /** + * Decodes and builds a resource object from a request body. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Drupal\jsonapi_resources\Unstable\NewResourceObject + * A new resource object. + */ + protected function extractResourceObjectFromRequest(Request $request) { + $decoded = $this->decodeRequestPayload($request); $primary_data = $decoded['data'] ?? []; if (isset($decoded['data'][0])) { // The `data` member of a request document can only be an array if it is @@ -65,8 +97,46 @@ final class DocumentResolver implements ArgumentValueResolverInterface { $message = sprintf($format, implode('`, `', $route_resource_types)); throw new AccessDeniedHttpException($message); } - $resource_object = NewResourceObject::createFromPrimaryData($resource_type, $primary_data); - yield new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), new LinkCollection([])); + return NewResourceObject::createFromPrimaryData($resource_type, $primary_data); + } + + /** + * Decodes a request payload. + * + * Mostly a duplication from the JSON:API module. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return mixed + * + * @see \Drupal\jsonapi\Controller\EntityResource::deserialize() + */ + protected function decodeRequestPayload(Request $request) { + $received = (string) $request->getContent(); + if (!$received) { + assert($request->isMethod('POST') || $request->isMethod('PATCH') || $request->isMethod('DELETE')); + $relationship_field_name = $request->attributes->get('_jsonapi_relationship_field_name'); + if ($request->isMethod('DELETE') && $relationship_field_name) { + throw new BadRequestHttpException(sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $relationship_field_name)); + } + else { + throw new BadRequestHttpException('Empty request body.'); + } + } + + // First decode the request data. We can then determine if the serialized + // data was malformed. + try { + $decoded = $this->serializer->decode($received, 'api_json'); + } + catch (UnexpectedValueException $e) { + // If an exception was thrown at this stage, there was a problem decoding + // the data. Throw a 400 HTTP exception. + throw new BadRequestHttpException($e->getMessage()); + } + + return $decoded; } } diff --git a/src/Unstable/EntityCreationTrait.php b/src/Unstable/EntityCreationTrait.php index a21101c..2d1fd62 100644 --- a/src/Unstable/EntityCreationTrait.php +++ b/src/Unstable/EntityCreationTrait.php @@ -3,9 +3,11 @@ namespace Drupal\jsonapi_resources\Unstable; use Drupal\Core\Entity\EntityInterface; +use Drupal\jsonapi\Entity\EntityValidationTrait; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\JsonApiResource\ResourceObject; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; /** @@ -19,6 +21,7 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; trait EntityCreationTrait { use ResourceObjectToEntityMapperAwareTrait; + use EntityValidationTrait; /** * Process the resource request. @@ -43,6 +46,17 @@ trait EntityCreationTrait { // Allow the class using this trait to modfiy the created entity before it // is saved. $this->modifyCreatedEntity($entity, $request); + + static::validate($entity); + + // Return a 409 Conflict response in accordance with the JSON:API spec. See + // http://jsonapi.org/format/#crud-creating-responses-409. + /* @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + if (!empty($entity_storage->loadByProperties(['uuid' => $entity->uuid()]))) { + throw new ConflictHttpException('Conflict: Entity already exists.'); + } + $entity->save(); $data = $this->createIndividualDataFromEntity($entity);