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 }
|