ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
FileUploadImpl.php
Go to the documentation of this file.
1<?php
2
19namespace ILIAS\FileUpload;
20
34use RecursiveArrayIterator;
35use RecursiveIteratorIterator;
37use ilFileUtils;
38
46final class FileUploadImpl implements FileUpload
47{
48 private bool $processed = false;
49 private bool $moved = false;
53 private array $uploadResult = [];
57 private array $rejectedUploadResult = [];
61 private ?array $uploadStreams = null;
62
63
64
65
73 public function __construct(private PreProcessorManager $processorManager, private Filesystems $filesystems, private GlobalHttpState $globalHttpState)
74 {
75 }
76
81 private function hardRemoveUpload(string $identifier, ProcessingStatus $status): never
82 {
83 // we delete the file from the temporary directory and remove it from the global $_FILES array
84 $file_stream = $this->uploadStreams[$identifier];
85 $uri = $file_stream->getMetadata('uri');
86 $file_stream->close();
87 unlink($uri);
88 unset($this->uploadStreams[$identifier]);
89 unset($_FILES[$identifier]);
90 throw new IllegalStateException($status->getMessage());
91 }
92
96 public function moveOneFileTo(UploadResult $uploadResult, string $destination, int $location = Location::STORAGE, string $file_name = '', bool $override_existing = false): bool
97 {
98 if (!$this->processed) {
99 throw new \RuntimeException('Can not move unprocessed files.');
100 }
101 $filesystem = $this->selectFilesystem($location);
102 $tempResults = [];
103
104 if ($uploadResult->getStatus()->getCode() === ProcessingStatus::REJECTED) {
105 return false;
106 }
107
108 try {
109 $path = rtrim($destination, "/") . '/' . ($file_name === "" ? $uploadResult->getName() : $file_name);
110 if ($override_existing && $filesystem->has($path)) {
111 $filesystem->delete($path);
112 }
113 $filesystem->writeStream($path, Streams::ofPsr7Stream($this->uploadStreams[$uploadResult->getPath()]));
114 $tempResults[] = $this->regenerateUploadResultWithPath($uploadResult, $path);
115 } catch (IOException $ex) {
116 $this->regenerateUploadResultWithCopyError($uploadResult, $ex->getMessage());
117 }
118
119 return true;
120 }
121
122
126 public function moveFilesTo(string $destination, int $location = Location::STORAGE): void
127 {
128 if (!$this->processed) {
129 throw new \RuntimeException('Can not move unprocessed files.');
130 }
131
132 if ($this->moved) {
133 throw new \RuntimeException('Can not move the files a second time.');
134 }
135
136 $filesystem = $this->selectFilesystem($location);
137 $tempResults = [];
138
139 foreach ($this->uploadResult as $key => $uploadResult) {
140 if ($uploadResult->getStatus()->getCode() === ProcessingStatus::REJECTED) {
141 continue;
142 }
143
144 try {
145 $path = $destination . '/' . $uploadResult->getName();
146 $filesystem->writeStream($path, Streams::ofPsr7Stream($this->uploadStreams[$key]));
147 $tempResults[] = $this->regenerateUploadResultWithPath($uploadResult, $path);
148 } catch (IOException $ex) {
149 $this->regenerateUploadResultWithCopyError($uploadResult, $ex->getMessage());
150 }
151 }
152
153 $this->uploadResult = $tempResults;
154 $this->uploadStreams = null;
155 $this->moved = true;
156 }
157
158
168 {
169 return new UploadResult(
170 $result->getName(),
171 $result->getSize(),
172 $result->getMimeType(),
173 $result->getMetaData(),
174 $result->getStatus(),
175 $path
176 );
177 }
178
179
188 private function regenerateUploadResultWithCopyError(UploadResult $result, string $errorReason): UploadResult
189 {
190 return new UploadResult(
191 $result->getName(),
192 $result->getSize(),
193 $result->getMimeType(),
194 $result->getMetaData(),
196 ''
197 );
198 }
199
200
210 private function selectFilesystem(int $location): Filesystem
211 {
212 return match ($location) {
213 Location::CUSTOMIZING => $this->filesystems->customizing(),
214 Location::STORAGE => $this->filesystems->storage(),
215 Location::WEB => $this->filesystems->web(),
216 Location::TEMPORARY => $this->filesystems->temp(),
217 default => throw new \InvalidArgumentException("No filesystem found for location code \"$location\""),
218 };
219 }
220
221
225 public function uploadSizeLimit(): int
226 {
228 }
229
230
234 public function register(PreProcessor $preProcessor): void
235 {
236 if (!$this->processed) {
237 $this->processorManager->with($preProcessor);
238 } else {
239 throw new IllegalStateException('Can not register processor after the upload was processed.');
240 }
241 }
242
243
247 public function process(): void
248 {
249 if ($this->processed) {
250 throw new IllegalStateException('Can not reprocess the uploaded files.');
251 }
252
253 $uploadedFiles = $this->globalHttpState->request()->getUploadedFiles();
254 $collectFilesFromNestedFields = $this->flattenUploadedFiles($uploadedFiles);
255 foreach ($collectFilesFromNestedFields as $file) {
256 $metadata = new Metadata($file->getClientFilename(), $file->getSize(), $file->getClientMediaType());
257 try {
258 $stream = Streams::ofPsr7Stream($file->getStream());
259 } catch (\RuntimeException) {
260 $this->rejectFailedUpload($metadata);
261 continue;
262 }
263
264 // we take the temporary file name as an identifier as it is the only unique attribute.
265 $identifier = $file->getStream()->getMetadata('uri');
266
267 $identifier = is_array($identifier) ? '' : $identifier;
268
269 $this->uploadStreams[$identifier] = $stream;
270
271 if ($file->getError() === UPLOAD_ERR_OK) {
272 $processingResult = $this->processorManager->process($stream, $metadata);
273
274 // we do discard if the result is a DENIED that there is no further pissibility to process the file.
275 if ($processingResult->getCode() === ProcessingStatus::DENIED) {
276 $this->hardRemoveUpload($identifier, $processingResult);
277 continue;
278 }
279
280 $result = new UploadResult(
281 $metadata->getFilename(),
282 $metadata->getUploadSize(),
283 $metadata->getMimeType(),
284 $metadata->additionalMetaData(),
285 $processingResult,
286 is_string($identifier) ? $identifier : ''
287 );
288 $this->uploadResult[$identifier] = $result;
289 } else {
290 $this->rejectFailedUpload($metadata);
291 }
292 }
293
294 $this->processed = true;
295 }
296
297
303 private function rejectFailedUpload(Metadata $metadata): void
304 {
305 //reject failed upload
306 $processingStatus = new ProcessingStatus(ProcessingStatus::REJECTED, 'Upload failed');
307 $extraMetadata = new ImmutableMapWrapper(new EntryLockingStringMap());
308 $result = new UploadResult(
309 $metadata->getFilename(),
310 $metadata->getUploadSize(),
311 $metadata->getMimeType(),
312 $extraMetadata,
313 $processingStatus,
314 ''
315 );
316
317 $this->rejectedUploadResult[] = $result;
318 }
319
320
324 public function getResults(): array
325 {
326 if ($this->processed) {
327 return array_merge($this->uploadResult, $this->rejectedUploadResult);
328 }
329
330 throw new IllegalStateException('Can not fetch results without processing the uploads.');
331 }
332
333
337 public function hasUploads(): bool
338 {
339 if ($this->moved) {
340 return false;
341 }
342
343 $uploadedFiles = $this->flattenUploadedFiles($this->globalHttpState->request()->getUploadedFiles());
344
345 return ($uploadedFiles !== []);
346 }
347
348
349 protected function flattenUploadedFiles(array $uploadedFiles): array
350 {
351 $recursiveIterator = new RecursiveIteratorIterator(
352 new RecursiveArrayIterator(
353 $uploadedFiles,
354 RecursiveArrayIterator::CHILD_ARRAYS_ONLY
355 ),
356 RecursiveIteratorIterator::LEAVES_ONLY
357 );
358
359 return iterator_to_array($recursiveIterator, false);
360 }
361
362
363 public function hasBeenProcessed(): bool
364 {
365 return $this->processed;
366 }
367}
$location
Definition: buildRTE.php:22
getFilename()
The filename supplied by the browser.
Definition: Metadata.php:76
getMimeType()
Client supplied mime type of the uploaded.
Definition: Metadata.php:118
getUploadSize()
This is always the original file size which was determined by the http service.
Definition: Metadata.php:106
hardRemoveUpload(string $identifier, ProcessingStatus $status)
@description This is the very last thing we can do if a preprocessor DENIEs an upload.
moveOneFileTo(UploadResult $uploadResult, string $destination, int $location=Location::STORAGE, string $file_name='', bool $override_existing=false)
Moves a single File (the attributes, metadata and upload-status of which are contained in UploadResul...
regenerateUploadResultWithCopyError(UploadResult $result, string $errorReason)
Creates a clone of the given result and set the status to rejected with the passed error message.
__construct(private PreProcessorManager $processorManager, private Filesystems $filesystems, private GlobalHttpState $globalHttpState)
FileUploadImpl constructor.
selectFilesystem(int $location)
Selects the correct filesystem by the given Location constant.
flattenUploadedFiles(array $uploadedFiles)
regenerateUploadResultWithPath(UploadResult $result, string $path)
Generate an exact copy of the result with the given path.
rejectFailedUpload(Metadata $metadata)
Reject a failed upload with the given metadata.
hasBeenProcessed()
Return (bool)true if the current upload has already been processed.
moveFilesTo(string $destination, int $location=Location::STORAGE)
@inheritDoc
Indicates general problems with the input or output operations.
Definition: IOException.php:28
Stream factory which enables the user to create streams without the knowledge of the concrete class.
Definition: Streams.php:32
static ofPsr7Stream(StreamInterface $stream)
Create a FileStream from a Psr7 compliant stream.
Definition: Streams.php:113
Class ilFileUtils.
static getPhpUploadSizeLimitInBytes()
const TEMPORARY
The ILIAS temporary directory.
Definition: Location.php:53
const CUSTOMIZING
The filesystem within the web root where all the skins and plugins are saved.
Definition: Location.php:48
const WEB
The filesystem within the ilias web root.
Definition: Location.php:38
const STORAGE
The filesystem outside of the ilias web root.
Definition: Location.php:43
The filesystem interface provides the public interface for the Filesystem service API consumer.
Definition: Filesystem.php:37
The Filesystems interface defines the access methods which can be used to fetch the different filesys...
Definition: Filesystems.php:30
The base interface for all filesystem streams.
Definition: FileStream.php:32
Interface GlobalHttpState.
$path
Definition: ltiservices.php:30
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static filesystems()
Returns the loaded filesystems.