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