annotate core/modules/jsonapi/src/Controller/FileUpload.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@18 1 <?php
Chris@18 2
Chris@18 3 namespace Drupal\jsonapi\Controller;
Chris@18 4
Chris@18 5 use Drupal\Component\Render\PlainTextOutput;
Chris@18 6 use Drupal\Core\Access\AccessResultReasonInterface;
Chris@18 7 use Drupal\Core\Cache\CacheableMetadata;
Chris@18 8 use Drupal\Core\Entity\EntityConstraintViolationListInterface;
Chris@18 9 use Drupal\Core\Entity\EntityFieldManagerInterface;
Chris@18 10 use Drupal\Core\Entity\FieldableEntityInterface;
Chris@18 11 use Drupal\Core\Field\FieldDefinitionInterface;
Chris@18 12 use Drupal\Core\Session\AccountInterface;
Chris@18 13 use Drupal\Core\Url;
Chris@18 14 use Drupal\jsonapi\Entity\EntityValidationTrait;
Chris@18 15 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
Chris@18 16 use Drupal\jsonapi\JsonApiResource\Link;
Chris@18 17 use Drupal\jsonapi\JsonApiResource\LinkCollection;
Chris@18 18 use Drupal\jsonapi\JsonApiResource\NullIncludedData;
Chris@18 19 use Drupal\jsonapi\JsonApiResource\ResourceObject;
Chris@18 20 use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
Chris@18 21 use Drupal\jsonapi\ResourceResponse;
Chris@18 22 use Drupal\jsonapi\ResourceType\ResourceType;
Chris@18 23 use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
Chris@18 24 use Symfony\Component\HttpFoundation\Request;
Chris@18 25 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
Chris@18 26 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Chris@18 27 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
Chris@18 28 use Symfony\Component\HttpKernel\HttpKernelInterface;
Chris@18 29 use Symfony\Component\Validator\ConstraintViolationInterface;
Chris@18 30
Chris@18 31 /**
Chris@18 32 * Handles file upload requests.
Chris@18 33 *
Chris@18 34 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
Chris@18 35 * may change at any time and could break any dependencies on it.
Chris@18 36 *
Chris@18 37 * @see https://www.drupal.org/project/jsonapi/issues/3032787
Chris@18 38 * @see jsonapi.api.php
Chris@18 39 */
Chris@18 40 class FileUpload {
Chris@18 41
Chris@18 42 use EntityValidationTrait;
Chris@18 43
Chris@18 44 /**
Chris@18 45 * The current user making the request.
Chris@18 46 *
Chris@18 47 * @var \Drupal\Core\Session\AccountInterface
Chris@18 48 */
Chris@18 49 protected $currentUser;
Chris@18 50
Chris@18 51 /**
Chris@18 52 * The field manager.
Chris@18 53 *
Chris@18 54 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
Chris@18 55 */
Chris@18 56 protected $fieldManager;
Chris@18 57
Chris@18 58 /**
Chris@18 59 * The file uploader.
Chris@18 60 *
Chris@18 61 * @var \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader
Chris@18 62 */
Chris@18 63 protected $fileUploader;
Chris@18 64
Chris@18 65 /**
Chris@18 66 * An HTTP kernel for making subrequests.
Chris@18 67 *
Chris@18 68 * @var \Symfony\Component\HttpKernel\HttpKernelInterface
Chris@18 69 */
Chris@18 70 protected $httpKernel;
Chris@18 71
Chris@18 72 /**
Chris@18 73 * Creates a new FileUpload instance.
Chris@18 74 *
Chris@18 75 * @param \Drupal\Core\Session\AccountInterface $current_user
Chris@18 76 * The current user.
Chris@18 77 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
Chris@18 78 * The entity field manager.
Chris@18 79 * @param \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader $file_uploader
Chris@18 80 * The file uploader.
Chris@18 81 * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
Chris@18 82 * An HTTP kernel for making subrequests.
Chris@18 83 */
Chris@18 84 public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, TemporaryJsonapiFileFieldUploader $file_uploader, HttpKernelInterface $http_kernel) {
Chris@18 85 $this->currentUser = $current_user;
Chris@18 86 $this->fieldManager = $field_manager;
Chris@18 87 $this->fileUploader = $file_uploader;
Chris@18 88 $this->httpKernel = $http_kernel;
Chris@18 89 }
Chris@18 90
Chris@18 91 /**
Chris@18 92 * Handles JSON:API file upload requests.
Chris@18 93 *
Chris@18 94 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@18 95 * The HTTP request object.
Chris@18 96 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 97 * The JSON:API resource type for the current request.
Chris@18 98 * @param string $file_field_name
Chris@18 99 * The file field for which the file is to be uploaded.
Chris@18 100 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
Chris@18 101 * The entity for which the file is to be uploaded.
Chris@18 102 *
Chris@18 103 * @return \Drupal\jsonapi\ResourceResponse
Chris@18 104 * The response object.
Chris@18 105 *
Chris@18 106 * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
Chris@18 107 * Thrown when there are validation errors.
Chris@18 108 * @throws \Drupal\Core\Entity\EntityStorageException
Chris@18 109 * Thrown if the upload's target resource could not be saved.
Chris@18 110 * @throws \Exception
Chris@18 111 * Thrown if an exception occurs during a subrequest to fetch the newly
Chris@18 112 * created file entity.
Chris@18 113 */
Chris@18 114 public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) {
Chris@18 115 $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);
Chris@18 116
Chris@18 117 static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity);
Chris@18 118
Chris@18 119 $filename = $this->fileUploader->validateAndParseContentDispositionHeader($request);
Chris@18 120 $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser);
Chris@18 121
Chris@18 122 if ($file instanceof EntityConstraintViolationListInterface) {
Chris@18 123 $violations = $file;
Chris@18 124 $message = "Unprocessable Entity: file validation failed.\n";
Chris@18 125 $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) {
Chris@18 126 return PlainTextOutput::renderFromHtml($violation->getMessage());
Chris@18 127 }, (array) $violations->getIterator()));
Chris@18 128 throw new UnprocessableEntityHttpException($message);
Chris@18 129 }
Chris@18 130
Chris@18 131 if ($field_definition->getFieldStorageDefinition()->getCardinality() === 1) {
Chris@18 132 $entity->{$file_field_name} = $file;
Chris@18 133 }
Chris@18 134 else {
Chris@18 135 $entity->get($file_field_name)->appendItem($file);
Chris@18 136 }
Chris@18 137 static::validate($entity, [$file_field_name]);
Chris@18 138 $entity->save();
Chris@18 139
Chris@18 140 $route_parameters = ['entity' => $entity->uuid()];
Chris@18 141 $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $file_field_name);
Chris@18 142 $related_url = Url::fromRoute($route_name, $route_parameters)->toString(TRUE);
Chris@18 143 $request = Request::create($related_url->getGeneratedUrl(), 'GET', [], $request->cookies->all(), [], $request->server->all());
Chris@18 144 return $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);
Chris@18 145 }
Chris@18 146
Chris@18 147 /**
Chris@18 148 * Handles JSON:API file upload requests.
Chris@18 149 *
Chris@18 150 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@18 151 * The HTTP request object.
Chris@18 152 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 153 * The JSON:API resource type for the current request.
Chris@18 154 * @param string $file_field_name
Chris@18 155 * The file field for which the file is to be uploaded.
Chris@18 156 *
Chris@18 157 * @return \Drupal\jsonapi\ResourceResponse
Chris@18 158 * The response object.
Chris@18 159 *
Chris@18 160 * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
Chris@18 161 * Thrown when there are validation errors.
Chris@18 162 */
Chris@18 163 public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) {
Chris@18 164 $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);
Chris@18 165
Chris@18 166 static::ensureFileUploadAccess($this->currentUser, $field_definition);
Chris@18 167
Chris@18 168 $filename = $this->fileUploader->validateAndParseContentDispositionHeader($request);
Chris@18 169 $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser);
Chris@18 170
Chris@18 171 if ($file instanceof EntityConstraintViolationListInterface) {
Chris@18 172 $violations = $file;
Chris@18 173 $message = "Unprocessable Entity: file validation failed.\n";
Chris@18 174 $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) {
Chris@18 175 return PlainTextOutput::renderFromHtml($violation->getMessage());
Chris@18 176 }, iterator_to_array($violations)));
Chris@18 177 throw new UnprocessableEntityHttpException($message);
Chris@18 178 }
Chris@18 179
Chris@18 180 // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
Chris@18 181 $self_link = new Link(new CacheableMetadata(), Url::fromRoute('jsonapi.file--file.individual', ['entity' => $file->uuid()]), ['self']);
Chris@18 182 /* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */
Chris@18 183 $links = new LinkCollection(['self' => $self_link]);
Chris@18 184
Chris@18 185 $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($file_field_name);
Chris@18 186 $file_resource_type = reset($relatable_resource_types);
Chris@18 187 $resource_object = ResourceObject::createFromEntity($file_resource_type, $file);
Chris@18 188 return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []);
Chris@18 189 }
Chris@18 190
Chris@18 191 /**
Chris@18 192 * Ensures that the given account is allowed to upload a file.
Chris@18 193 *
Chris@18 194 * @param \Drupal\Core\Session\AccountInterface $account
Chris@18 195 * The account for which access should be checked.
Chris@18 196 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
Chris@18 197 * The field for which the file is to be uploaded.
Chris@18 198 * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity
Chris@18 199 * The entity, if one exists, for which the file is to be uploaded.
Chris@18 200 */
Chris@18 201 protected static function ensureFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, FieldableEntityInterface $entity = NULL) {
Chris@18 202 $access_result = $entity
Chris@18 203 ? TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition, $entity)
Chris@18 204 : TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition);
Chris@18 205 if (!$access_result->isAllowed()) {
Chris@18 206 $reason = 'The current user is not permitted to upload a file for this field.';
Chris@18 207 if ($access_result instanceof AccessResultReasonInterface) {
Chris@18 208 $reason .= ' ' . $access_result->getReason();
Chris@18 209 }
Chris@18 210 throw new AccessDeniedHttpException($reason);
Chris@18 211 }
Chris@18 212 }
Chris@18 213
Chris@18 214 /**
Chris@18 215 * Validates and loads a field definition instance.
Chris@18 216 *
Chris@18 217 * @param string $entity_type_id
Chris@18 218 * The entity type ID the field is attached to.
Chris@18 219 * @param string $bundle
Chris@18 220 * The bundle the field is attached to.
Chris@18 221 * @param string $field_name
Chris@18 222 * The field name.
Chris@18 223 *
Chris@18 224 * @return \Drupal\Core\Field\FieldDefinitionInterface
Chris@18 225 * The field definition.
Chris@18 226 *
Chris@18 227 * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
Chris@18 228 * Thrown when the field does not exist.
Chris@18 229 * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
Chris@18 230 * Thrown when the target type of the field is not a file, or the current
Chris@18 231 * user does not have 'edit' access for the field.
Chris@18 232 */
Chris@18 233 protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name) {
Chris@18 234 $field_definitions = $this->fieldManager->getFieldDefinitions($entity_type_id, $bundle);
Chris@18 235 if (!isset($field_definitions[$field_name])) {
Chris@18 236 throw new NotFoundHttpException(sprintf('Field "%s" does not exist.', $field_name));
Chris@18 237 }
Chris@18 238
Chris@18 239 /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
Chris@18 240 $field_definition = $field_definitions[$field_name];
Chris@18 241 if ($field_definition->getSetting('target_type') !== 'file') {
Chris@18 242 throw new AccessDeniedException(sprintf('"%s" is not a file field', $field_name));
Chris@18 243 }
Chris@18 244
Chris@18 245 return $field_definition;
Chris@18 246 }
Chris@18 247
Chris@18 248 }