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 */
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;
82 private readonly Config
$config;
84 /** @param bool|string $name file name or false */
85 public function __construct(bool|
string $name = false)
87 $this->config
= Config
::getInstance();
88 if ($name && is_string($name)) {
89 $this->setName($name);
92 if (! extension_loaded('zip')) {
96 $this->zipExtension
= new ZipExtension(new ZipArchive());
102 * @see File::cleanUp()
104 public function __destruct()
110 * deletes file if it is temporary, usually from a moved upload file
112 public function cleanUp(): bool
114 if ($this->isTemp()) {
115 return $this->delete();
124 public function delete(): bool
126 return unlink((string) $this->getName());
130 * checks or sets the temp flag for this file
131 * file objects with temp flags are deleted with object destruction
133 * @param bool $isTemp sets the temp flag
135 public function isTemp(bool|
null $isTemp = null): bool
137 if ($isTemp !== null) {
138 $this->isTemp
= $isTemp;
141 return $this->isTemp
;
147 * @param string|null $name file name
149 public function setName(string|
null $name): void
151 $this->name
= trim((string) $name);
157 * @return string|false the binary file content, or false if no content
159 public function getRawContent(): string|
false
161 if ($this->content
!== null) {
162 return $this->content
;
165 if ($this->isUploaded() && ! $this->checkUploadedFile()) {
169 if (! $this->isReadable()) {
173 $this->content
= file_get_contents((string) $this->getName());
175 return $this->content
;
181 * @return string|false the binary file content as a string,
182 * or false if no content
184 public function getContent(): string|
false
186 $result = $this->getRawContent();
187 if ($result === false) {
191 return '0x' . bin2hex($result);
195 * Whether file is uploaded.
197 public function isUploaded(): bool
199 if ($this->getName() === null) {
203 return is_uploaded_file($this->getName());
209 * @return string|null File::$_name
211 public function getName(): string|
null
217 * Initializes object from uploaded file.
219 * @param string $name name of file uploaded
221 public function setUploadedFile(string $name): bool
223 $this->setName($name);
225 if (! $this->isUploaded()) {
226 $this->setName(null);
227 $this->errorMessage
= Message
::error(__('File was not an uploaded file.'));
236 * Loads uploaded file from table change request.
238 * @param string $key the md5 hash of the column name
239 * @param string $rownumber number of row to process
241 public function setUploadedFromTblChangeRequest(
246 ! isset($_FILES['fields_upload'])
247 ||
empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])
252 $file = $this->fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
254 switch ($file['error']) {
256 return $this->setUploadedFile($file['tmp_name']);
258 case UPLOAD_ERR_NO_FILE
:
260 case UPLOAD_ERR_INI_SIZE
:
261 $this->errorMessage
= Message
::error(__(
262 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
265 case UPLOAD_ERR_FORM_SIZE
:
266 $this->errorMessage
= Message
::error(__(
267 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
270 case UPLOAD_ERR_PARTIAL
:
271 $this->errorMessage
= Message
::error(__(
272 'The uploaded file was only partially uploaded.',
275 case UPLOAD_ERR_NO_TMP_DIR
:
276 $this->errorMessage
= Message
::error(__('Missing a temporary folder.'));
278 case UPLOAD_ERR_CANT_WRITE
:
279 $this->errorMessage
= Message
::error(__('Failed to write file to disk.'));
281 case UPLOAD_ERR_EXTENSION
:
282 $this->errorMessage
= Message
::error(__('File upload stopped by extension.'));
285 $this->errorMessage
= Message
::error(__('Unknown error in file upload.'));
292 * strips some dimension from the multi-dimensional array from $_FILES
295 * $file['name']['multi_edit'][$rownumber][$key] = [value]
296 * $file['type']['multi_edit'][$rownumber][$key] = [value]
297 * $file['size']['multi_edit'][$rownumber][$key] = [value]
298 * $file['tmp_name']['multi_edit'][$rownumber][$key] = [value]
299 * $file['error']['multi_edit'][$rownumber][$key] = [value]
303 * $file['name'] = [value]
304 * $file['type'] = [value]
305 * $file['size'] = [value]
306 * $file['tmp_name'] = [value]
307 * $file['error'] = [value]
310 * @param mixed[] $file the array
311 * @param string $rownumber number of row to process
312 * @param string $key key to process
316 public function fetchUploadedFromTblChangeRequestMultiple(
322 'name' => $file['name']['multi_edit'][$rownumber][$key],
323 'type' => $file['type']['multi_edit'][$rownumber][$key],
324 'size' => $file['size']['multi_edit'][$rownumber][$key],
325 'tmp_name' => $file['tmp_name']['multi_edit'][$rownumber][$key],
326 'error' => $file['error']['multi_edit'][$rownumber][$key],
331 * sets the name if the file to the one selected in the tbl_change form
333 * @param string $key the md5 hash of the column name
334 * @param string|null $rownumber number of row to process
336 public function setSelectedFromTblChangeRequest(
338 string|
null $rownumber = null,
341 ! empty($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
342 && is_string($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
344 // ... whether with multiple rows ...
345 return $this->setLocalSelectedFile($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key]);
352 * Returns possible error message.
354 * @return Message|null error message
356 public function getError(): Message|
null
358 return $this->errorMessage
;
362 * Checks whether there was any error.
364 public function isError(): bool
366 return $this->errorMessage
!== null;
370 * checks the superglobals provided if the tbl_change form is submitted
371 * and uses the submitted/selected file
373 * @param string $key the md5 hash of the column name
374 * @param string $rownumber number of row to process
376 public function checkTblChangeForm(string $key, string $rownumber): bool
378 if ($this->setUploadedFromTblChangeRequest($key, $rownumber)) {
380 $this->errorMessage
= null;
385 if ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
387 $this->errorMessage
= null;
392 // all failed, whether just no file uploaded/selected or an error
398 * Sets named file to be read from UploadDir.
400 * @param string $name file name
402 public function setLocalSelectedFile(string $name): bool
404 if ($this->config
->settings
['UploadDir'] === '') {
409 Util
::userDir($this->config
->settings
['UploadDir']) . Core
::securePath($name),
411 if (@is_link
((string) $this->getName())) {
412 $this->errorMessage
= Message
::error(__('File is a symbolic link'));
413 $this->setName(null);
418 if (! $this->isReadable()) {
419 $this->errorMessage
= Message
::error(__('File could not be read!'));
420 $this->setName(null);
429 * Checks whether file can be read.
431 public function isReadable(): bool
433 // suppress warnings from being displayed, but not from being logged
434 // any file access outside of open_basedir will issue a warning
435 return @is_readable
((string) $this->getName());
439 * If we are on a server with open_basedir, we must move the file
440 * before opening it. The FAQ 1.11 explains how to create the "./tmp"
441 * directory - if needed
443 * @todo move check of $cfg['TempDir'] into Config?
445 public function checkUploadedFile(): bool
447 if ($this->isReadable()) {
451 $tmpSubdir = $this->config
->getUploadTempDir();
452 if ($tmpSubdir === null) {
453 // cannot create directory or access, point user to FAQ 1.11
454 $this->errorMessage
= Message
::error(__(
455 'Error moving the uploaded file, see [doc@faq1-11]FAQ 1.11[/doc].',
461 $newFileToUpload = (string) tempnam(
463 basename((string) $this->getName()),
466 // suppress warnings from being displayed, but not from being logged
467 // any file access outside of open_basedir will issue a warning
469 $moveUploadedFileResult = move_uploaded_file(
470 (string) $this->getName(),
474 if (! $moveUploadedFileResult) {
475 $this->errorMessage
= Message
::error(__('Error while moving uploaded file.'));
480 $this->setName($newFileToUpload);
483 if (! $this->isReadable()) {
484 $this->errorMessage
= Message
::error(__('Cannot read uploaded file.'));
493 * Detects what compression the file uses
495 * @return string|false false on error, otherwise string MIME type of
496 * compression, none for none
498 * @todo move file read part into readChunk() or getChunk()
499 * @todo add support for compression plugins
501 protected function detectCompression(): string|
false
503 // suppress warnings from being displayed, but not from being logged
504 // f.e. any file access outside of open_basedir will issue a warning
506 $file = fopen((string) $this->getName(), 'rb');
510 $this->errorMessage
= Message
::error(__('File could not be read!'));
515 $this->compression
= Util
::getCompressionMimeType($file);
517 return $this->compression
;
521 * Sets whether the content should be decompressed before returned
523 * @param bool $decompress whether to decompress
525 public function setDecompressContent(bool $decompress): void
527 $this->decompress
= $decompress;
531 * Returns the file handle
533 * @return resource|null file handle
535 public function getHandle()
537 if ($this->handle
=== null) {
541 return $this->handle
;
545 * Sets the file handle
547 * @param resource $handle file handle
549 public function setHandle($handle): void
551 $this->handle
= $handle;
555 * Sets error message for unsupported compression.
557 public function errorUnsupported(): void
559 $this->errorMessage
= Message
::error(sprintf(
561 'You attempted to load file with unsupported compression (%s). '
562 . 'Either support for it is not implemented or disabled by your '
565 $this->getCompression(),
570 * Attempts to open the file.
572 public function open(): bool
574 if (! $this->decompress
) {
575 $this->handle
= @fopen
((string) $this->getName(), 'r');
578 switch ($this->getCompression()) {
582 case 'application/bzip2':
583 if (! $this->config
->settings
['BZipDump'] ||
! function_exists('bzopen')) {
584 $this->errorUnsupported();
589 $this->handle
= @bzopen
($this->getName(), 'r');
591 case 'application/gzip':
592 if (! $this->config
->settings
['GZipDump'] ||
! function_exists('gzopen')) {
593 $this->errorUnsupported();
598 $this->handle
= @gzopen
((string) $this->getName(), 'r');
600 case 'application/zip':
601 if ($this->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 ($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 return $this->compression ??
$this->detectCompression();
731 * @return int the offset
733 public function getOffset(): int
735 return $this->offset
;
739 * Returns the chunk size
741 * @return int the chunk size
743 public function getChunkSize(): int
745 return $this->chunkSize
;
749 * Sets the chunk size
751 * @param int $chunkSize the chunk size
753 public function setChunkSize(int $chunkSize): void
755 $this->chunkSize
= $chunkSize;
759 * Returns the length of the content in the file
761 * @return int the length of the file content
763 public function getContentLength(): int
765 return strlen($this->content ??
'');