3 declare(strict_types
=1);
10 use function basename
;
14 use function extension_loaded
;
17 use function file_get_contents
;
20 use function function_exists
;
24 use function is_readable
;
25 use function is_string
;
26 use function is_uploaded_file
;
27 use function mb_strcut
;
28 use function move_uploaded_file
;
29 use function ob_end_clean
;
30 use function ob_start
;
37 use const UPLOAD_ERR_CANT_WRITE
;
38 use const UPLOAD_ERR_EXTENSION
;
39 use const UPLOAD_ERR_FORM_SIZE
;
40 use const UPLOAD_ERR_INI_SIZE
;
41 use const UPLOAD_ERR_NO_FILE
;
42 use const UPLOAD_ERR_NO_TMP_DIR
;
43 use const UPLOAD_ERR_OK
;
44 use const UPLOAD_ERR_PARTIAL
;
49 * @todo when uploading a file into a blob field, should we also consider using
50 * chunks like in import? UPDATE `table` SET `field` = `field` + [chunk]
54 /** @var string|null the temporary file name */
55 protected string|
null $name = null;
57 protected string|
null $content = null;
59 /** @var Message|null the error message */
60 protected Message|
null $errorMessage = null;
62 /** @var bool whether the file is temporary or not */
63 protected bool $isTemp = false;
65 protected string|
null $compression = null;
67 protected int $offset = 0;
69 /** @var int size of chunk to read with every step */
70 protected int $chunkSize = 32768;
72 /** @var resource|null file handle */
73 protected $handle = null;
75 /** @var bool whether to decompress content before returning */
76 protected bool $decompress = false;
78 /** @var string charset of file */
79 protected string $charset = '';
81 private ZipExtension
$zipExtension;
83 /** @param bool|string $name file name or false */
84 public function __construct(bool|
string $name = false)
86 if ($name && is_string($name)) {
87 $this->setName($name);
90 if (! extension_loaded('zip')) {
94 $this->zipExtension
= new ZipExtension(new ZipArchive());
100 * @see File::cleanUp()
102 public function __destruct()
108 * deletes file if it is temporary, usually from a moved upload file
110 public function cleanUp(): bool
112 if ($this->isTemp()) {
113 return $this->delete();
122 public function delete(): bool
124 return unlink((string) $this->getName());
128 * checks or sets the temp flag for this file
129 * file objects with temp flags are deleted with object destruction
131 * @param bool $isTemp sets the temp flag
133 public function isTemp(bool|
null $isTemp = null): bool
135 if ($isTemp !== null) {
136 $this->isTemp
= $isTemp;
139 return $this->isTemp
;
145 * @param string|null $name file name
147 public function setName(string|
null $name): void
149 $this->name
= trim((string) $name);
155 * @return string|false the binary file content, or false if no content
157 public function getRawContent(): string|
false
159 if ($this->content
!== null) {
160 return $this->content
;
163 if ($this->isUploaded() && ! $this->checkUploadedFile()) {
167 if (! $this->isReadable()) {
171 $this->content
= file_get_contents((string) $this->getName());
173 return $this->content
;
179 * @return string|false the binary file content as a string,
180 * or false if no content
182 public function getContent(): string|
false
184 $result = $this->getRawContent();
185 if ($result === false) {
189 return '0x' . bin2hex($result);
193 * Whether file is uploaded.
195 public function isUploaded(): bool
197 if ($this->getName() === null) {
201 return is_uploaded_file($this->getName());
207 * @return string|null File::$_name
209 public function getName(): string|
null
215 * Initializes object from uploaded file.
217 * @param string $name name of file uploaded
219 public function setUploadedFile(string $name): bool
221 $this->setName($name);
223 if (! $this->isUploaded()) {
224 $this->setName(null);
225 $this->errorMessage
= Message
::error(__('File was not an uploaded file.'));
234 * Loads uploaded file from table change request.
236 * @param string $key the md5 hash of the column name
237 * @param string $rownumber number of row to process
239 public function setUploadedFromTblChangeRequest(
244 ! isset($_FILES['fields_upload'])
245 ||
empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])
250 $file = $this->fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
252 switch ($file['error']) {
254 return $this->setUploadedFile($file['tmp_name']);
256 case UPLOAD_ERR_NO_FILE
:
258 case UPLOAD_ERR_INI_SIZE
:
259 $this->errorMessage
= Message
::error(__(
260 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
263 case UPLOAD_ERR_FORM_SIZE
:
264 $this->errorMessage
= Message
::error(__(
265 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
268 case UPLOAD_ERR_PARTIAL
:
269 $this->errorMessage
= Message
::error(__(
270 'The uploaded file was only partially uploaded.',
273 case UPLOAD_ERR_NO_TMP_DIR
:
274 $this->errorMessage
= Message
::error(__('Missing a temporary folder.'));
276 case UPLOAD_ERR_CANT_WRITE
:
277 $this->errorMessage
= Message
::error(__('Failed to write file to disk.'));
279 case UPLOAD_ERR_EXTENSION
:
280 $this->errorMessage
= Message
::error(__('File upload stopped by extension.'));
283 $this->errorMessage
= Message
::error(__('Unknown error in file upload.'));
290 * strips some dimension from the multi-dimensional array from $_FILES
293 * $file['name']['multi_edit'][$rownumber][$key] = [value]
294 * $file['type']['multi_edit'][$rownumber][$key] = [value]
295 * $file['size']['multi_edit'][$rownumber][$key] = [value]
296 * $file['tmp_name']['multi_edit'][$rownumber][$key] = [value]
297 * $file['error']['multi_edit'][$rownumber][$key] = [value]
301 * $file['name'] = [value]
302 * $file['type'] = [value]
303 * $file['size'] = [value]
304 * $file['tmp_name'] = [value]
305 * $file['error'] = [value]
308 * @param mixed[] $file the array
309 * @param string $rownumber number of row to process
310 * @param string $key key to process
314 public function fetchUploadedFromTblChangeRequestMultiple(
320 'name' => $file['name']['multi_edit'][$rownumber][$key],
321 'type' => $file['type']['multi_edit'][$rownumber][$key],
322 'size' => $file['size']['multi_edit'][$rownumber][$key],
323 'tmp_name' => $file['tmp_name']['multi_edit'][$rownumber][$key],
324 'error' => $file['error']['multi_edit'][$rownumber][$key],
329 * sets the name if the file to the one selected in the tbl_change form
331 * @param string $key the md5 hash of the column name
332 * @param string $rownumber number of row to process
334 public function setSelectedFromTblChangeRequest(
336 string|
null $rownumber = null,
339 ! empty($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
340 && is_string($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
342 // ... whether with multiple rows ...
343 return $this->setLocalSelectedFile($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key]);
350 * Returns possible error message.
352 * @return Message|null error message
354 public function getError(): Message|
null
356 return $this->errorMessage
;
360 * Checks whether there was any error.
362 public function isError(): bool
364 return $this->errorMessage
!== null;
368 * checks the superglobals provided if the tbl_change form is submitted
369 * and uses the submitted/selected file
371 * @param string $key the md5 hash of the column name
372 * @param string $rownumber number of row to process
374 public function checkTblChangeForm(string $key, string $rownumber): bool
376 if ($this->setUploadedFromTblChangeRequest($key, $rownumber)) {
378 $this->errorMessage
= null;
383 if ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
385 $this->errorMessage
= null;
390 // all failed, whether just no file uploaded/selected or an error
396 * Sets named file to be read from UploadDir.
398 * @param string $name file name
400 public function setLocalSelectedFile(string $name): bool
402 $config = Config
::getInstance();
403 if ($config->settings
['UploadDir'] === '') {
408 Util
::userDir($config->settings
['UploadDir']) . Core
::securePath($name),
410 if (@is_link
((string) $this->getName())) {
411 $this->errorMessage
= Message
::error(__('File is a symbolic link'));
412 $this->setName(null);
417 if (! $this->isReadable()) {
418 $this->errorMessage
= Message
::error(__('File could not be read!'));
419 $this->setName(null);
428 * Checks whether file can be read.
430 public function isReadable(): bool
432 // suppress warnings from being displayed, but not from being logged
433 // any file access outside of open_basedir will issue a warning
434 return @is_readable
((string) $this->getName());
438 * If we are on a server with open_basedir, we must move the file
439 * before opening it. The FAQ 1.11 explains how to create the "./tmp"
440 * directory - if needed
442 * @todo move check of $cfg['TempDir'] into Config?
444 public function checkUploadedFile(): bool
446 if ($this->isReadable()) {
450 $tmpSubdir = Config
::getInstance()->getUploadTempDir();
451 if ($tmpSubdir === null) {
452 // cannot create directory or access, point user to FAQ 1.11
453 $this->errorMessage
= Message
::error(__(
454 'Error moving the uploaded file, see [doc@faq1-11]FAQ 1.11[/doc].',
460 $newFileToUpload = (string) tempnam(
462 basename((string) $this->getName()),
465 // suppress warnings from being displayed, but not from being logged
466 // any file access outside of open_basedir will issue a warning
468 $moveUploadedFileResult = move_uploaded_file(
469 (string) $this->getName(),
473 if (! $moveUploadedFileResult) {
474 $this->errorMessage
= Message
::error(__('Error while moving uploaded file.'));
479 $this->setName($newFileToUpload);
482 if (! $this->isReadable()) {
483 $this->errorMessage
= Message
::error(__('Cannot read uploaded file.'));
492 * Detects what compression the file uses
494 * @return string|false false on error, otherwise string MIME type of
495 * compression, none for none
497 * @todo move file read part into readChunk() or getChunk()
498 * @todo add support for compression plugins
500 protected function detectCompression(): string|
false
502 // suppress warnings from being displayed, but not from being logged
503 // f.e. any file access outside of open_basedir will issue a warning
505 $file = fopen((string) $this->getName(), 'rb');
509 $this->errorMessage
= Message
::error(__('File could not be read!'));
514 $this->compression
= Util
::getCompressionMimeType($file);
516 return $this->compression
;
520 * Sets whether the content should be decompressed before returned
522 * @param bool $decompress whether to decompress
524 public function setDecompressContent(bool $decompress): void
526 $this->decompress
= $decompress;
530 * Returns the file handle
532 * @return resource|null file handle
534 public function getHandle()
536 if ($this->handle
=== null) {
540 return $this->handle
;
544 * Sets the file handle
546 * @param resource $handle file handle
548 public function setHandle($handle): void
550 $this->handle
= $handle;
554 * Sets error message for unsupported compression.
556 public function errorUnsupported(): void
558 $this->errorMessage
= Message
::error(sprintf(
560 'You attempted to load file with unsupported compression (%s). '
561 . 'Either support for it is not implemented or disabled by your '
564 $this->getCompression(),
569 * Attempts to open the file.
571 public function open(): bool
573 if (! $this->decompress
) {
574 $this->handle
= @fopen
((string) $this->getName(), 'r');
577 $config = Config
::getInstance();
578 switch ($this->getCompression()) {
582 case 'application/bzip2':
583 if (! $config->settings
['BZipDump'] ||
! function_exists('bzopen')) {
584 $this->errorUnsupported();
589 $this->handle
= @bzopen
($this->getName(), 'r');
591 case 'application/gzip':
592 if (! $config->settings
['GZipDump'] ||
! function_exists('gzopen')) {
593 $this->errorUnsupported();
598 $this->handle
= @gzopen
((string) $this->getName(), 'r');
600 case 'application/zip':
601 if ($config->settings
['ZipDump'] && function_exists('zip_open')) {
602 return $this->openZip();
605 $this->errorUnsupported();
610 $this->handle
= @fopen
((string) $this->getName(), 'r');
613 $this->errorUnsupported();
618 return $this->handle
!== false;
622 * Opens file from zip
624 * @param string|null $specificEntry Entry to open
626 public function openZip(string|
null $specificEntry = null): bool
628 $result = $this->zipExtension
->getContents($this->getName(), $specificEntry);
629 if (! empty($result['error'])) {
630 $this->errorMessage
= Message
::rawError($result['error']);
635 $this->content
= $result['data'];
642 * Checks whether we've reached end of file
644 public function eof(): bool
646 if ($this->handle
!== null) {
647 return feof($this->handle
);
650 return $this->offset
== strlen($this->content ??
'');
656 public function close(): void
658 if ($this->handle
!== null) {
659 fclose($this->handle
);
660 $this->handle
= null;
670 * Reads data from file
672 * @param int $size Number of bytes to read
674 public function read(int $size): string
676 if ($this->compression
=== 'application/zip') {
677 $result = mb_strcut($this->content ??
'', $this->offset
, $size);
678 $this->offset +
= strlen($result);
683 if ($this->handle
=== null) {
687 if ($this->compression
=== 'application/bzip2') {
688 return (string) bzread($this->handle
, $size);
691 if ($this->compression
=== 'application/gzip') {
692 return (string) gzread($this->handle
, $size);
695 return (string) fread($this->handle
, $size);
699 * Returns the character set of the file
701 * @return string character set of the file
703 public function getCharset(): string
705 return $this->charset
;
709 * Sets the character set of the file
711 * @param string $charset character set of the file
713 public function setCharset(string $charset): void
715 $this->charset
= $charset;
719 * Returns compression used by file.
721 * @return string MIME type of compression, none for none
723 public function getCompression(): string
725 if ($this->compression
=== null) {
726 return $this->detectCompression();
729 return $this->compression
;
735 * @return int the offset
737 public function getOffset(): int
739 return $this->offset
;
743 * Returns the chunk size
745 * @return int the chunk size
747 public function getChunkSize(): int
749 return $this->chunkSize
;
753 * Sets the chunk size
755 * @param int $chunkSize the chunk size
757 public function setChunkSize(int $chunkSize): void
759 $this->chunkSize
= $chunkSize;
763 * Returns the length of the content in the file
765 * @return int the length of the file content
767 public function getContentLength(): int
769 return strlen($this->content ??
'');