Translated using Weblate (Portuguese (Brazil))
[phpmyadmin.git] / src / File.php
blob3d34a11c48dddb1bc2fb909de62e84162b8683c8
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use ZipArchive;
9 use function __;
10 use function basename;
11 use function bin2hex;
12 use function bzopen;
13 use function bzread;
14 use function extension_loaded;
15 use function fclose;
16 use function feof;
17 use function file_get_contents;
18 use function fopen;
19 use function fread;
20 use function function_exists;
21 use function gzopen;
22 use function gzread;
23 use function is_link;
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;
31 use function sprintf;
32 use function strlen;
33 use function tempnam;
34 use function trim;
35 use function unlink;
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;
46 /**
47 * File wrapper class
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]
52 class File
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')) {
91 return;
94 $this->zipExtension = new ZipExtension(new ZipArchive());
97 /**
98 * destructor
100 * @see File::cleanUp()
102 public function __destruct()
104 $this->cleanUp();
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();
116 return true;
120 * deletes the file
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;
143 * accessor
145 * @param string|null $name file name
147 public function setName(string|null $name): void
149 $this->name = trim((string) $name);
153 * Gets file content
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()) {
164 return false;
167 if (! $this->isReadable()) {
168 return false;
171 $this->content = file_get_contents((string) $this->getName());
173 return $this->content;
177 * Gets file 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) {
186 return false;
189 return '0x' . bin2hex($result);
193 * Whether file is uploaded.
195 public function isUploaded(): bool
197 if ($this->getName() === null) {
198 return false;
201 return is_uploaded_file($this->getName());
205 * accessor
207 * @return string|null File::$_name
209 public function getName(): string|null
211 return $this->name;
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.'));
227 return false;
230 return true;
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(
240 string $key,
241 string $rownumber,
242 ): bool {
243 if (
244 ! isset($_FILES['fields_upload'])
245 || empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])
247 return false;
250 $file = $this->fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
252 switch ($file['error']) {
253 case UPLOAD_ERR_OK:
254 return $this->setUploadedFile($file['tmp_name']);
256 case UPLOAD_ERR_NO_FILE:
257 break;
258 case UPLOAD_ERR_INI_SIZE:
259 $this->errorMessage = Message::error(__(
260 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
262 break;
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.',
267 break;
268 case UPLOAD_ERR_PARTIAL:
269 $this->errorMessage = Message::error(__(
270 'The uploaded file was only partially uploaded.',
272 break;
273 case UPLOAD_ERR_NO_TMP_DIR:
274 $this->errorMessage = Message::error(__('Missing a temporary folder.'));
275 break;
276 case UPLOAD_ERR_CANT_WRITE:
277 $this->errorMessage = Message::error(__('Failed to write file to disk.'));
278 break;
279 case UPLOAD_ERR_EXTENSION:
280 $this->errorMessage = Message::error(__('File upload stopped by extension.'));
281 break;
282 default:
283 $this->errorMessage = Message::error(__('Unknown error in file upload.'));
286 return false;
290 * strips some dimension from the multi-dimensional array from $_FILES
292 * <code>
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]
299 * // becomes:
301 * $file['name'] = [value]
302 * $file['type'] = [value]
303 * $file['size'] = [value]
304 * $file['tmp_name'] = [value]
305 * $file['error'] = [value]
306 * </code>
308 * @param mixed[] $file the array
309 * @param string $rownumber number of row to process
310 * @param string $key key to process
312 * @return mixed[]
314 public function fetchUploadedFromTblChangeRequestMultiple(
315 array $file,
316 string $rownumber,
317 string $key,
318 ): array {
319 return [
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(
335 string $key,
336 string|null $rownumber = null,
337 ): bool {
338 if (
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]);
346 return false;
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)) {
377 // well done ...
378 $this->errorMessage = null;
380 return true;
383 if ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
384 // well done ...
385 $this->errorMessage = null;
387 return true;
390 // all failed, whether just no file uploaded/selected or an error
392 return false;
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'] === '') {
404 return false;
407 $this->setName(
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);
414 return false;
417 if (! $this->isReadable()) {
418 $this->errorMessage = Message::error(__('File could not be read!'));
419 $this->setName(null);
421 return false;
424 return true;
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()) {
447 return true;
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].',
457 return false;
460 $newFileToUpload = (string) tempnam(
461 $tmpSubdir,
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
467 ob_start();
468 $moveUploadedFileResult = move_uploaded_file(
469 (string) $this->getName(),
470 $newFileToUpload,
472 ob_end_clean();
473 if (! $moveUploadedFileResult) {
474 $this->errorMessage = Message::error(__('Error while moving uploaded file.'));
476 return false;
479 $this->setName($newFileToUpload);
480 $this->isTemp(true);
482 if (! $this->isReadable()) {
483 $this->errorMessage = Message::error(__('Cannot read uploaded file.'));
485 return false;
488 return true;
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
504 ob_start();
505 $file = fopen((string) $this->getName(), 'rb');
506 ob_end_clean();
508 if (! $file) {
509 $this->errorMessage = Message::error(__('File could not be read!'));
511 return false;
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) {
537 $this->open();
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 '
562 . 'configuration.',
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()) {
579 case false:
580 return false;
582 case 'application/bzip2':
583 if (! $config->settings['BZipDump'] || ! function_exists('bzopen')) {
584 $this->errorUnsupported();
586 return false;
589 $this->handle = @bzopen($this->getName(), 'r');
590 break;
591 case 'application/gzip':
592 if (! $config->settings['GZipDump'] || ! function_exists('gzopen')) {
593 $this->errorUnsupported();
595 return false;
598 $this->handle = @gzopen((string) $this->getName(), 'r');
599 break;
600 case 'application/zip':
601 if ($config->settings['ZipDump'] && function_exists('zip_open')) {
602 return $this->openZip();
605 $this->errorUnsupported();
607 return false;
609 case 'none':
610 $this->handle = @fopen((string) $this->getName(), 'r');
611 break;
612 default:
613 $this->errorUnsupported();
615 return false;
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']);
632 return false;
635 $this->content = $result['data'];
636 $this->offset = 0;
638 return true;
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 ?? '');
654 * Closes the file
656 public function close(): void
658 if ($this->handle !== null) {
659 fclose($this->handle);
660 $this->handle = null;
661 } else {
662 $this->content = '';
663 $this->offset = 0;
666 $this->cleanUp();
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);
680 return $result;
683 if ($this->handle === null) {
684 return '';
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;
733 * Returns the offset
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 ?? '');