-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
ResourceTypeConverter.php
353 lines (312 loc) · 15 KB
/
ResourceTypeConverter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
<?php
namespace Neos\Flow\ResourceManagement;
/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
use Neos\Flow\Log\Utility\LogEnvironment;
use Psr\Http\Message\UploadedFileInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Error\Messages\Error as FlowError;
use Neos\Http\Factories\FlowUploadedFile;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Flow\Property\Exception\InvalidPropertyMappingConfigurationException;
use Neos\Flow\Property\PropertyMappingConfigurationInterface;
use Neos\Flow\Property\TypeConverter\AbstractTypeConverter;
use Neos\Utility\Files;
use Psr\Log\LoggerInterface;
/**
* A type converter for converting strings, array and uploaded files to PersistentResource objects.
*
* Has two major working modes:
*
* 1. File Uploads by PHP
*
* In this case, the input array is expected to be a fresh file upload following the native PHP handling. The
* temporary upload file is then imported through the resource manager.
*
* To enable the handling of files that have already been uploaded earlier, the special field ['originallySubmittedResource']
* is checked. If set, it is used to fetch a file that has already been uploaded even if no file has been actually uploaded in the current request.
*
*
* 2. Strings / arbitrary Arrays
*
* If the source
* - is an array and contains the key '__identity'
*
* the converter will find an existing resource with the given identity or continue and assign the given identity if
* CONFIGURATION_IDENTITY_CREATION_ALLOWED is set.
*
* - is a string looking like a SHA1 (40 characters [0-9a-f]) or
* - is an array and contains the key 'hash' with a value looking like a SHA1 (40 characters [0-9a-f])
*
* the converter will look up an existing PersistentResource with that hash and return it if found. If that fails,
* the converter will try to import a file named like that hash from the configured CONFIGURATION_RESOURCE_LOAD_PATH.
*
* If no hash is given in an array source but the key 'data' is set, the content of that key is assumed a binary string
* and a PersistentResource representing this content is created and returned.
*
* The imported PersistentResource will be given a 'filename' if set in the source array in both cases (import from file or data).
*
* @Flow\Scope("singleton")
*/
class ResourceTypeConverter extends AbstractTypeConverter
{
/**
* @var string
*/
const CONFIGURATION_RESOURCE_LOAD_PATH = 'resourceLoadPath';
/**
* @var integer
*/
const CONFIGURATION_IDENTITY_CREATION_ALLOWED = 1;
/**
* Sets the default resource collection name (see Settings: Neos.Flow.resource.collections) to use for this resource,
* will fallback to ResourceManager::DEFAULT_PERSISTENT_COLLECTION_NAME
*
* @var string
*/
const CONFIGURATION_COLLECTION_NAME = 'collectionName';
/**
* @var array<string>
*/
protected $sourceTypes = ['string', 'array', UploadedFileInterface::class];
/**
* @var string
*/
protected $targetType = PersistentResource::class;
/**
* @var integer
*/
protected $priority = 1;
/**
* @Flow\Inject
* @var ResourceManager
*/
protected $resourceManager;
/**
* @Flow\Inject
* @var ResourceRepository
*/
protected $resourceRepository;
/**
* @Flow\Inject
* @var PersistenceManagerInterface
*/
protected $persistenceManager;
/**
* @Flow\Inject(name="Neos.Flow:SystemLogger")
* @var LoggerInterface
*/
protected $logger;
/**
* @var array
*/
protected $convertedResources = [];
/**
* Injects the (system) logger based on PSR-3.
*
* @param LoggerInterface $logger
* @return void
*/
public function injectLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Converts the given string or array to a PersistentResource object.
*
* If the input format is an array, this method assumes the resource to be a
* fresh file upload and imports the temporary upload file through the
* ResourceManager.
*
* Note that $source['error'] will also be present if a file was successfully
* uploaded. In that case its value will be \UPLOAD_ERR_OK.
*
* @param array|string|UploadedFileInterface $source The upload info (expected keys: error, name, tmp_name), the hash or an UploadedFile
* @param string $targetType
* @param array $convertedChildProperties
* @param PropertyMappingConfigurationInterface|null $configuration
* @return PersistentResource|null|FlowError if the input format is not supported or could not be converted for other reasons
* @throws Exception
* @throws Exception\InvalidResourceDataException
* @throws InvalidPropertyMappingConfigurationException
*/
public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
{
if (empty($source)) {
return null;
}
if ($source instanceof UploadedFileInterface) {
return $this->handleUploadedFile($source, $configuration);
}
if (is_string($source)) {
$source = ['hash' => $source];
}
// $source is ALWAYS an array at this point
if (isset($source['error']) || isset($source['originallySubmittedResource'])) {
return $this->handleFileUploads($source, $configuration);
} elseif (isset($source['hash']) || isset($source['data'])) {
return $this->handleHashAndData($source, $configuration);
}
return null;
}
/**
* @param array $source
* @param PropertyMappingConfigurationInterface|null $configuration
* @return PersistentResource|null|FlowError
*/
protected function handleFileUploads(array $source, PropertyMappingConfigurationInterface $configuration = null)
{
if (!isset($source['error']) || $source['error'] === \UPLOAD_ERR_NO_FILE) {
if (isset($source['originallySubmittedResource']) && isset($source['originallySubmittedResource']['__identity'])) {
/** @var PersistentResource|null $resource */
$resource = $this->persistenceManager->getObjectByIdentifier($source['originallySubmittedResource']['__identity'], PersistentResource::class);
return $resource;
}
return null;
}
if ($source['error'] !== \UPLOAD_ERR_OK) {
switch ($source['error']) {
case \UPLOAD_ERR_INI_SIZE:
case \UPLOAD_ERR_FORM_SIZE:
case \UPLOAD_ERR_PARTIAL:
return new FlowError(Files::getUploadErrorMessage($source['error']), 1264440823);
default:
$this->logger->error(sprintf('A server error occurred while converting an uploaded resource: "%s"', Files::getUploadErrorMessage($source['error'])), LogEnvironment::fromMethodName(__METHOD__));
return new FlowError('An error occurred while uploading. Please try again or contact the administrator if the problem remains', 1340193849);
}
}
if (isset($this->convertedResources[$source['tmp_name']])) {
return $this->convertedResources[$source['tmp_name']];
}
try {
$resource = $this->resourceManager->importUploadedResource($source, $this->getCollectionName($source, $configuration));
$this->convertedResources[$source['tmp_name']] = $resource;
return $resource;
} catch (\Exception $exception) {
$this->logger->warning('Could not import an uploaded file', ['exception' => $exception] + LogEnvironment::fromMethodName(__METHOD__));
return new FlowError('During import of an uploaded file an error occurred. See log for more details.', 1264517906);
}
}
/**
* @param array $source
* @param PropertyMappingConfigurationInterface|null $configuration
* @return PersistentResource|FlowError
* @throws Exception
* @throws Exception\InvalidResourceDataException
* @throws InvalidPropertyMappingConfigurationException
*/
protected function handleHashAndData(array $source, PropertyMappingConfigurationInterface $configuration = null)
{
$hash = null;
$resource = null;
$givenResourceIdentity = null;
if (isset($source['__identity'])) {
$givenResourceIdentity = $source['__identity'];
unset($source['__identity']);
$resource = $this->resourceRepository->findByIdentifier($givenResourceIdentity);
if ($resource instanceof PersistentResource) {
return $resource;
}
if ($configuration === null || $configuration->getConfigurationValue(ResourceTypeConverter::class, self::CONFIGURATION_IDENTITY_CREATION_ALLOWED) !== true) {
throw new InvalidPropertyMappingConfigurationException('Creation of resource objects with identity not allowed. To enable this, you need to set the PropertyMappingConfiguration Value "CONFIGURATION_IDENTITY_CREATION_ALLOWED" to true');
}
}
if (isset($source['hash']) && preg_match('/[0-9a-f]{40}/', $source['hash'])) {
$hash = $source['hash'];
}
if ($hash !== null && count($source) === 1) {
$resource = $this->resourceManager->getResourceBySha1($hash);
}
if ($resource === null) {
$collectionName = $source['collectionName'] ?? $this->getCollectionName($source, $configuration);
if (isset($source['data']) && isset($source['filename'])) {
$resource = $this->resourceManager->importResourceFromContent(base64_decode($source['data']), $source['filename'], $collectionName, $givenResourceIdentity);
} elseif ($hash !== null) {
$resource = $this->resourceManager->importResource($configuration->getConfigurationValue(ResourceTypeConverter::class, self::CONFIGURATION_RESOURCE_LOAD_PATH) . '/' . $hash, $collectionName, $givenResourceIdentity);
if (is_array($source) && isset($source['filename'])) {
$resource->setFilename($source['filename']);
}
}
if ($hash !== null && $resource->getSha1() !== $hash) {
throw new Exception\InvalidResourceDataException('The source SHA1 did not match the SHA1 of the imported resource.', 1482248149);
}
}
if ($resource instanceof PersistentResource) {
return $resource;
}
return new FlowError('The resource manager could not create a PersistentResource instance.', 1404312901);
}
/**
* @param UploadedFileInterface $source
* @param PropertyMappingConfigurationInterface|null $configuration
* @return PersistentResource|null|FlowError
*/
protected function handleUploadedFile(UploadedFileInterface $source, PropertyMappingConfigurationInterface $configuration = null)
{
if ($source instanceof FlowUploadedFile && $source->getError() === UPLOAD_ERR_NO_FILE && $source->getOriginallySubmittedResource() !== null) {
$identifier = is_array($source->getOriginallySubmittedResource()) ? $source->getOriginallySubmittedResource()['__identity'] : $source->getOriginallySubmittedResource();
/** @var PersistentResource|null $resource */
$resource = $this->persistenceManager->getObjectByIdentifier($identifier, PersistentResource::class);
return $resource;
}
switch ($source->getError()) {
case \UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
return null;
case \UPLOAD_ERR_INI_SIZE:
case \UPLOAD_ERR_FORM_SIZE:
case \UPLOAD_ERR_PARTIAL:
return new FlowError(Files::getUploadErrorMessage($source->getError()), 1264440823);
default:
$this->logger->error(sprintf('A server error occurred while converting an uploaded resource: "%s"', Files::getUploadErrorMessage($source['error'])), LogEnvironment::fromMethodName(__METHOD__));
return new FlowError('An error occurred while uploading. Please try again or contact the administrator if the problem remains', 1340193849);
}
if (isset($this->convertedResources[spl_object_hash($source)])) {
return $this->convertedResources[spl_object_hash($source)];
}
try {
$resource = $this->resourceManager->importResource($source->getStream()->detach(), $this->getCollectionName($source, $configuration));
$resource->setFilename($source->getClientFilename());
$this->convertedResources[spl_object_hash($source)] = $resource;
return $resource;
} catch (\Exception $exception) {
$this->logger->warning('Could not import an uploaded file', ['exception' => $exception] + LogEnvironment::fromMethodName(__METHOD__));
return new FlowError('During import of an uploaded file an error occurred. See log for more details.', 1264517906);
}
}
/**
* Get the collection name this resource will be stored in. Default will be ResourceManager::DEFAULT_PERSISTENT_COLLECTION_NAME
* The propertyMappingConfiguration CONFIGURATION_COLLECTION_NAME will directly override the default. Then if CONFIGURATION_ALLOW_COLLECTION_OVERRIDE is true
* and __collectionName is in the $source this will finally be the value.
*
* @param array|UploadedFileInterface $source
* @param PropertyMappingConfigurationInterface|null $configuration
* @return string
* @throws InvalidPropertyMappingConfigurationException
*/
protected function getCollectionName($source, PropertyMappingConfigurationInterface $configuration = null)
{
if ($configuration === null) {
return ResourceManager::DEFAULT_PERSISTENT_COLLECTION_NAME;
}
$collectionName = $configuration->getConfigurationValue(ResourceTypeConverter::class, self::CONFIGURATION_COLLECTION_NAME) ?: ResourceManager::DEFAULT_PERSISTENT_COLLECTION_NAME;
if ($source instanceof FlowUploadedFile && $source->getCollectionName() !== null) {
$collectionName = $source->getCollectionName();
}
if (is_array($source) && isset($source['__collectionName']) && $source['__collectionName'] !== '') {
$collectionName = $source['__collectionName'];
}
if ($this->resourceManager->getCollection($collectionName) === null) {
throw new InvalidPropertyMappingConfigurationException(sprintf('The selected resource collection named "%s" does not exist, a resource could not be imported.', $collectionName), 1416687475);
}
return $collectionName;
}
}