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