ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
FileUploadImpl.php
Go to the documentation of this file.
1<?php
2
19namespace ILIAS\FileUpload;
20
34use RecursiveArrayIterator;
35use RecursiveIteratorIterator;
37use ilFileUtils;
39
47final class FileUploadImpl implements FileUpload
48{
49 private bool $processed = false;
50 private bool $moved = false;
54 private array $uploadResult = [];
58 private array $rejectedUploadResult = [];
62 private ?array $uploadStreams = null;
63
64
65
66
74 public function __construct(
75 private PreProcessorManager $processorManager,
76 private Filesystems $filesystems,
77 private GlobalHttpState $globalHttpState,
78 private ?PhpUploadLimit $upload_limit = null
79 ) {
80 }
81
86 private function hardRemoveUpload(string $identifier, ProcessingStatus $status): never
87 {
88 // we delete the file from the temporary directory and remove it from the global $_FILES array
89 $file_stream = $this->uploadStreams[$identifier];
90 $uri = $file_stream->getMetadata('uri');
91 $file_stream->close();
92 unlink($uri);
93 unset($this->uploadStreams[$identifier]);
94 unset($_FILES[$identifier]);
95 throw new IllegalStateException($status->getMessage());
96 }
97
101 public function moveOneFileTo(UploadResult $uploadResult, string $destination, int $location = Location::STORAGE, string $file_name = '', bool $override_existing = false): bool
102 {
103 if (!$this->processed) {
104 throw new \RuntimeException('Can not move unprocessed files.');
105 }
106 $filesystem = $this->selectFilesystem($location);
107 $tempResults = [];
108
109 if ($uploadResult->getStatus()->getCode() === ProcessingStatus::REJECTED) {
110 return false;
111 }
112
113 try {
114 $path = rtrim($destination, "/") . '/' . ($file_name === "" ? $uploadResult->getName() : $file_name);
115 if ($override_existing && $filesystem->has($path)) {
116 $filesystem->delete($path);
117 }
118 $filesystem->writeStream($path, Streams::ofPsr7Stream($this->uploadStreams[$uploadResult->getPath()]));
119 $tempResults[] = $this->regenerateUploadResultWithPath($uploadResult, $path);
120 } catch (IOException $ex) {
121 $this->regenerateUploadResultWithCopyError($uploadResult, $ex->getMessage());
122 }
123
124 return true;
125 }
126
127
131 public function moveFilesTo(string $destination, int $location = Location::STORAGE): void
132 {
133 if (!$this->processed) {
134 throw new \RuntimeException('Can not move unprocessed files.');
135 }
136
137 if ($this->moved) {
138 throw new \RuntimeException('Can not move the files a second time.');
139 }
140
141 $filesystem = $this->selectFilesystem($location);
142 $tempResults = [];
143
144 foreach ($this->uploadResult as $key => $uploadResult) {
145 if ($uploadResult->getStatus()->getCode() === ProcessingStatus::REJECTED) {
146 continue;
147 }
148
149 try {
150 $path = $destination . '/' . $uploadResult->getName();
151 $filesystem->writeStream($path, Streams::ofPsr7Stream($this->uploadStreams[$key]));
152 $tempResults[] = $this->regenerateUploadResultWithPath($uploadResult, $path);
153 } catch (IOException $ex) {
154 $this->regenerateUploadResultWithCopyError($uploadResult, $ex->getMessage());
155 }
156 }
157
158 $this->uploadResult = $tempResults;
159 $this->uploadStreams = null;
160 $this->moved = true;
161 }
162
163
172 private function regenerateUploadResultWithPath(UploadResult $result, string $path): UploadResult
173 {
174 return new UploadResult(
175 $result->getName(),
176 $result->getSize(),
177 $result->getMimeType(),
178 $result->getMetaData(),
179 $result->getStatus(),
180 $path
181 );
182 }
183
184
193 private function regenerateUploadResultWithCopyError(UploadResult $result, string $errorReason): UploadResult
194 {
195 return new UploadResult(
196 $result->getName(),
197 $result->getSize(),
198 $result->getMimeType(),
199 $result->getMetaData(),
201 ''
202 );
203 }
204
205
215 private function selectFilesystem(int $location): Filesystem
216 {
217 return match ($location) {
218 Location::CUSTOMIZING => $this->filesystems->customizing(),
219 Location::STORAGE => $this->filesystems->storage(),
220 Location::WEB => $this->filesystems->web(),
221 Location::TEMPORARY => $this->filesystems->temp(),
222 default => throw new \InvalidArgumentException("No filesystem found for location code \"$location\""),
223 };
224 }
225
226
230 public function uploadSizeLimit(): int
231 {
232 return $this->upload_limit?->getPhpUploadLimitInBytes() ?? ilFileUtils::getPhpUploadSizeLimitInBytes();
233 }
234
235
239 public function register(PreProcessor $preProcessor): void
240 {
241 if (!$this->processed) {
242 $this->processorManager->with($preProcessor);
243 } else {
244 throw new IllegalStateException('Can not register processor after the upload was processed.');
245 }
246 }
247
248
252 public function process(): void
253 {
254 if ($this->processed) {
255 throw new IllegalStateException('Can not reprocess the uploaded files.');
256 }
257
258 $uploadedFiles = $this->globalHttpState->request()->getUploadedFiles();
259 $collectFilesFromNestedFields = $this->flattenUploadedFiles($uploadedFiles);
260 foreach ($collectFilesFromNestedFields as $file) {
261 $metadata = new Metadata($file->getClientFilename(), $file->getSize(), $file->getClientMediaType());
262 try {
263 $stream = Streams::ofPsr7Stream($file->getStream());
264 } catch (\RuntimeException) {
265 $this->rejectFailedUpload($metadata);
266 continue;
267 }
268
269 // we take the temporary file name as an identifier as it is the only unique attribute.
270 $identifier = $file->getStream()->getMetadata('uri');
271
272 $identifier = is_array($identifier) ? '' : $identifier;
273
274 $this->uploadStreams[$identifier] = $stream;
275
276 if ($file->getError() === UPLOAD_ERR_OK) {
277 $processingResult = $this->processorManager->process($stream, $metadata);
278
279 // we do discard if the result is a DENIED that there is no further pissibility to process the file.
280 if ($processingResult->getCode() === ProcessingStatus::DENIED) {
281 $this->hardRemoveUpload($identifier, $processingResult);
282 continue;
283 }
284
285 $result = new UploadResult(
286 $metadata->getFilename(),
287 $metadata->getUploadSize(),
288 $metadata->getMimeType(),
289 $metadata->additionalMetaData(),
290 $processingResult,
291 is_string($identifier) ? $identifier : ''
292 );
293 $this->uploadResult[$identifier] = $result;
294 } else {
295 $this->rejectFailedUpload($metadata);
296 }
297 }
298
299 $this->processed = true;
300 }
301
302
308 private function rejectFailedUpload(Metadata $metadata): void
309 {
310 //reject failed upload
311 $processingStatus = new ProcessingStatus(ProcessingStatus::REJECTED, 'Upload failed');
312 $extraMetadata = new ImmutableMapWrapper(new EntryLockingStringMap());
313 $result = new UploadResult(
314 $metadata->getFilename(),
315 $metadata->getUploadSize(),
316 $metadata->getMimeType(),
317 $extraMetadata,
318 $processingStatus,
319 ''
320 );
321
322 $this->rejectedUploadResult[] = $result;
323 }
324
325
329 public function getResults(): array
330 {
331 if ($this->processed) {
332 return array_merge($this->uploadResult, $this->rejectedUploadResult);
333 }
334
335 throw new IllegalStateException('Can not fetch results without processing the uploads.');
336 }
337
338
342 public function hasUploads(): bool
343 {
344 if ($this->moved) {
345 return false;
346 }
347
348 $uploadedFiles = $this->flattenUploadedFiles($this->globalHttpState->request()->getUploadedFiles());
349
350 return ($uploadedFiles !== []);
351 }
352
353
354 private function flattenUploadedFiles(array $uploadedFiles): array
355 {
356 $recursiveIterator = new RecursiveIteratorIterator(
357 new RecursiveArrayIterator(
358 $uploadedFiles,
359 RecursiveArrayIterator::CHILD_ARRAYS_ONLY
360 ),
361 RecursiveIteratorIterator::LEAVES_ONLY
362 );
363
364 return iterator_to_array($recursiveIterator, false);
365 }
366
367
368 public function hasBeenProcessed(): bool
369 {
370 return $this->processed;
371 }
372}
$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
__construct(private PreProcessorManager $processorManager, private Filesystems $filesystems, private GlobalHttpState $globalHttpState, private ?PhpUploadLimit $upload_limit=null)
FileUploadImpl constructor.
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.
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:121
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.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static filesystems()
Returns the loaded filesystems.