annotate core/modules/jsonapi/src/Controller/FileUpload.php @ 5:12f9dff5fda9 tip

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