ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
FileUploadImpl.php
Go to the documentation of this file.
1 <?php
2 
19 namespace ILIAS\FileUpload;
20 
39 use ilFileUtils;
40 
48 final class FileUploadImpl implements FileUpload
49 {
53  private bool $processed;
54  private bool $moved;
58  private array $uploadResult;
62  private array $rejectedUploadResult;
66  private ?array $uploadStreams = null;
67 
68 
69 
70 
78  public function __construct(PreProcessorManager $processorManager, Filesystems $filesystems, GlobalHttpState $globalHttpState)
79  {
80  $this->processorManager = $processorManager;
81  $this->filesystems = $filesystems;
82  $this->globalHttpState = $globalHttpState;
83  $this->processed = false;
84  $this->moved = false;
85  $this->uploadResult = [];
86  $this->rejectedUploadResult = [];
87  }
88 
93  private function hardRemoveUpload(string $identifier, string $rejection_message): void
94  {
95  // we delete the file from the temporary directory and remove it from the global $_FILES array
96  $file_stream = $this->uploadStreams[$identifier];
97  $uri = $file_stream->getMetadata('uri');
98  $file_stream->close();
99  unlink($uri);
100  unset($this->uploadStreams[$identifier]);
101  unset($_FILES[$identifier]);
102  throw new IllegalStateException($rejection_message);
103  }
104 
108  public function moveOneFileTo(UploadResult $uploadResult, string $destination, int $location = Location::STORAGE, string $file_name = '', bool $override_existing = false): bool
109  {
110  if (!$this->processed) {
111  throw new \RuntimeException('Can not move unprocessed files.');
112  }
113  $filesystem = $this->selectFilesystem($location);
114  $tempResults = [];
115 
116  if ($uploadResult->getStatus()->getCode() == ProcessingStatus::REJECTED) {
117  return false;
118  }
119 
120  try {
121  $path = rtrim($destination, "/") . '/' . ($file_name == "" ? $uploadResult->getName() : $file_name);
122  if ($override_existing && $filesystem->has($path)) {
123  $filesystem->delete($path);
124  }
125  $filesystem->writeStream($path, Streams::ofPsr7Stream($this->uploadStreams[$uploadResult->getPath()]));
126  $tempResults[] = $this->regenerateUploadResultWithPath($uploadResult, $path);
127  } catch (IOException $ex) {
128  $this->regenerateUploadResultWithCopyError($uploadResult, $ex->getMessage());
129  }
130 
131  return true;
132  }
133 
134 
138  public function moveFilesTo(string $destination, int $location = Location::STORAGE): void
139  {
140  if (!$this->processed) {
141  throw new \RuntimeException('Can not move unprocessed files.');
142  }
143 
144  if ($this->moved) {
145  throw new \RuntimeException('Can not move the files a second time.');
146  }
147 
148  $filesystem = $this->selectFilesystem($location);
149  $tempResults = [];
150 
151  foreach ($this->uploadResult as $key => $uploadResult) {
152  if ($uploadResult->getStatus()->getCode() == ProcessingStatus::REJECTED) {
153  continue;
154  }
155 
156  try {
157  $path = $destination . '/' . $uploadResult->getName();
158  $filesystem->writeStream($path, Streams::ofPsr7Stream($this->uploadStreams[$key]));
159  $tempResults[] = $this->regenerateUploadResultWithPath($uploadResult, $path);
160  } catch (IOException $ex) {
161  $this->regenerateUploadResultWithCopyError($uploadResult, $ex->getMessage());
162  }
163  }
164 
165  $this->uploadResult = $tempResults;
166  $this->uploadStreams = null;
167  $this->moved = true;
168  }
169 
170 
180  {
181  return new UploadResult(
182  $result->getName(),
183  $result->getSize(),
184  $result->getMimeType(),
185  $result->getMetaData(),
186  $result->getStatus(),
187  $path
188  );
189  }
190 
191 
200  private function regenerateUploadResultWithCopyError(UploadResult $result, string $errorReason): UploadResult
201  {
202  return new UploadResult(
203  $result->getName(),
204  $result->getSize(),
205  $result->getMimeType(),
206  $result->getMetaData(),
207  new ProcessingStatus(ProcessingStatus::REJECTED, $errorReason),
208  ''
209  );
210  }
211 
212 
222  private function selectFilesystem(int $location): Filesystem
223  {
224  switch ($location) {
226  return $this->filesystems->customizing();
227  case Location::STORAGE:
228  return $this->filesystems->storage();
229  case Location::WEB:
230  return $this->filesystems->web();
231  case Location::TEMPORARY:
232  return $this->filesystems->temp();
233  default:
234  throw new \InvalidArgumentException("No filesystem found for location code \"$location\"");
235  }
236  }
237 
238 
242  public function uploadSizeLimit(): int
243  {
245  }
246 
247 
251  public function register(PreProcessor $preProcessor): void
252  {
253  if (!$this->processed) {
254  $this->processorManager->with($preProcessor);
255  } else {
256  throw new IllegalStateException('Can not register processor after the upload was processed.');
257  }
258  }
259 
260 
264  public function process(): void
265  {
266  if ($this->processed) {
267  throw new IllegalStateException('Can not reprocess the uploaded files.');
268  }
269 
270  $uploadedFiles = $this->globalHttpState->request()->getUploadedFiles();
271  $collectFilesFromNestedFields = $this->flattenUploadedFiles($uploadedFiles);
272  foreach ($collectFilesFromNestedFields as $file) {
273  $metadata = new Metadata($file->getClientFilename(), $file->getSize(), $file->getClientMediaType());
274  try {
275  $stream = Streams::ofPsr7Stream($file->getStream());
276  } catch (\RuntimeException $e) {
277  $this->rejectFailedUpload($metadata);
278  continue;
279  }
280 
281  // we take the temporary file name as an identifier as it is the only unique attribute.
282  $identifier = $file->getStream()->getMetadata('uri');
283 
284  $identifier = is_array($identifier) ? '' : $identifier;
285 
286  $this->uploadStreams[$identifier] = $stream;
287 
288  if ($file->getError() === UPLOAD_ERR_OK) {
289  $processingResult = $this->processorManager->process($stream, $metadata);
290 
291  // we do discard if the result is a DENIED that there is no further pissibility to process the file.
292  if ($processingResult->getCode() === ProcessingStatus::DENIED) {
293  $this->hardRemoveUpload($identifier, $processingResult->getMessage());
294  continue;
295  }
296 
297  $result = new UploadResult(
298  $metadata->getFilename(),
299  $metadata->getUploadSize(),
300  $metadata->getMimeType(),
301  $metadata->additionalMetaData(),
302  $processingResult,
303  is_string($identifier) ? $identifier : ''
304  );
305  $this->uploadResult[$identifier] = $result;
306  } else {
307  $this->rejectFailedUpload($metadata);
308  }
309  }
310 
311  $this->processed = true;
312  }
313 
314 
320  private function rejectFailedUpload(Metadata $metadata): void
321  {
322  //reject failed upload
323  $processingStatus = new ProcessingStatus(ProcessingStatus::REJECTED, 'Upload failed');
324  $extraMetadata = new ImmutableMapWrapper(new EntryLockingStringMap());
325  $result = new UploadResult(
326  $metadata->getFilename(),
327  $metadata->getUploadSize(),
328  $metadata->getMimeType(),
329  $extraMetadata,
330  $processingStatus,
331  ''
332  );
333 
334  $this->rejectedUploadResult[] = $result;
335  }
336 
337 
341  public function getResults(): array
342  {
343  if ($this->processed) {
344  return array_merge($this->uploadResult, $this->rejectedUploadResult);
345  }
346 
347  throw new IllegalStateException('Can not fetch results without processing the uploads.');
348  }
349 
350 
354  public function hasUploads(): bool
355  {
356  if ($this->moved) {
357  return false;
358  }
359 
360  $uploadedFiles = $this->flattenUploadedFiles($this->globalHttpState->request()->getUploadedFiles());
361 
362  return ($uploadedFiles !== []);
363  }
364 
365 
366  protected function flattenUploadedFiles(array $uploadedFiles): array
367  {
368  $recursiveIterator = new RecursiveIteratorIterator(
370  $uploadedFiles,
371  RecursiveArrayIterator::CHILD_ARRAYS_ONLY
372  ),
373  RecursiveIteratorIterator::LEAVES_ONLY
374  );
375 
376  return iterator_to_array($recursiveIterator, false);
377  }
378 
379 
380  public function hasBeenProcessed(): bool
381  {
382  return $this->processed;
383  }
384 }
Interface GlobalHttpState.
const REJECTED
Upload got rejected by a processor.
const STORAGE
The filesystem outside of the ilias web root.
Definition: Location.php:40
regenerateUploadResultWithCopyError(UploadResult $result, string $errorReason)
Creates a clone of the given result and set the status to rejected with the passed error message...
$location
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: buildRTE.php:22
selectFilesystem(int $location)
Selects the correct filesystem by the given Location constant.
flattenUploadedFiles(array $uploadedFiles)
hardRemoveUpload(string $identifier, string $rejection_message)
This is the very last thing we can do if a preprocessor DENIEs an upload.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
__construct(PreProcessorManager $processorManager, Filesystems $filesystems, GlobalHttpState $globalHttpState)
FileUploadImpl constructor.
PreProcessorManager $processorManager
getMimeType()
Client supplied mime type of the uploaded.
Definition: Metadata.php:102
static getUploadSizeLimitBytes()
$path
Definition: ltiservices.php:32
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getFilename()
The filename supplied by the browser.
Definition: Metadata.php:60
moveFilesTo(string $destination, int $location=Location::STORAGE)
getUploadSize()
This is always the original file size which was determined by the http service.
Definition: Metadata.php:90
static filesystems()
Returns the loaded filesystems.
rejectFailedUpload(Metadata $metadata)
Reject a failed upload with the given metadata.
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...
string $key
Consumer key/client ID value.
Definition: System.php:193
const CUSTOMIZING
The filesystem within the web root where all the skins and plugins are saved.
Definition: Location.php:45
hasBeenProcessed()
Return (bool)true if the current upload has already been processed.
const DENIED
Upload got denied by a processor, the upload will be removed immediately.
Class FileUpload.
Definition: FileUpload.php:34
regenerateUploadResultWithPath(UploadResult $result, string $path)
Generate an exact copy of the result with the given path.
const TEMPORARY
The ILIAS temporary directory.
Definition: Location.php:50
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Class FlySystemFileAccessTest disabled disabled disabled.
const WEB
The filesystem within the ilias web root.
Definition: Location.php:35
Class Filesystems.
Definition: Filesystems.php:29
static ofPsr7Stream(StreamInterface $stream)
Create a FileStream from a Psr7 compliant stream.
Definition: Streams.php:82