Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / File.php
blob1f96e534d721681b524ff565e5e1c05369875c82
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;
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')) {
93 return;
96 $this->zipExtension = new ZipExtension(new ZipArchive());
99 /**
100 * destructor
102 * @see File::cleanUp()
104 public function __destruct()
106 $this->cleanUp();
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();
118 return true;
122 * deletes the file
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;
145 * accessor
147 * @param string|null $name file name
149 public function setName(string|null $name): void
151 $this->name = trim((string) $name);
155 * Gets file content
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()) {
166 return false;
169 if (! $this->isReadable()) {
170 return false;
173 $this->content = file_get_contents((string) $this->getName());
175 return $this->content;
179 * Gets file 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) {
188 return false;
191 return '0x' . bin2hex($result);
195 * Whether file is uploaded.
197 public function isUploaded(): bool
199 if ($this->getName() === null) {
200 return false;
203 return is_uploaded_file($this->getName());
207 * accessor
209 * @return string|null File::$_name
211 public function getName(): string|null
213 return $this->name;
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.'));
229 return false;
232 return true;
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(
242 string $key,
243 string $rownumber,
244 ): bool {
245 if (
246 ! isset($_FILES['fields_upload'])
247 || empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])
249 return false;
252 $file = $this->fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
254 switch ($file['error']) {
255 case UPLOAD_ERR_OK:
256 return $this->setUploadedFile($file['tmp_name']);
258 case UPLOAD_ERR_NO_FILE:
259 break;
260 case UPLOAD_ERR_INI_SIZE:
261 $this->errorMessage = Message::error(__(
262 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
264 break;
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.',
269 break;
270 case UPLOAD_ERR_PARTIAL:
271 $this->errorMessage = Message::error(__(
272 'The uploaded file was only partially uploaded.',
274 break;
275 case UPLOAD_ERR_NO_TMP_DIR:
276 $this->errorMessage = Message::error(__('Missing a temporary folder.'));
277 break;
278 case UPLOAD_ERR_CANT_WRITE:
279 $this->errorMessage = Message::error(__('Failed to write file to disk.'));
280 break;
281 case UPLOAD_ERR_EXTENSION:
282 $this->errorMessage = Message::error(__('File upload stopped by extension.'));
283 break;
284 default:
285 $this->errorMessage = Message::error(__('Unknown error in file upload.'));
288 return false;
292 * strips some dimension from the multi-dimensional array from $_FILES
294 * <code>
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]
301 * // becomes:
303 * $file['name'] = [value]
304 * $file['type'] = [value]
305 * $file['size'] = [value]
306 * $file['tmp_name'] = [value]
307 * $file['error'] = [value]
308 * </code>
310 * @param mixed[] $file the array
311 * @param string $rownumber number of row to process
312 * @param string $key key to process
314 * @return mixed[]
316 public function fetchUploadedFromTblChangeRequestMultiple(
317 array $file,
318 string $rownumber,
319 string $key,
320 ): array {
321 return [
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(
337 string $key,
338 string|null $rownumber = null,
339 ): bool {
340 if (
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]);
348 return false;
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)) {
379 // well done ...
380 $this->errorMessage = null;
382 return true;
385 if ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
386 // well done ...
387 $this->errorMessage = null;
389 return true;
392 // all failed, whether just no file uploaded/selected or an error
394 return false;
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'] === '') {
405 return false;
408 $this->setName(
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);
415 return false;
418 if (! $this->isReadable()) {
419 $this->errorMessage = Message::error(__('File could not be read!'));
420 $this->setName(null);
422 return false;
425 return true;
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()) {
448 return true;
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].',
458 return false;
461 $newFileToUpload = (string) tempnam(
462 $tmpSubdir,
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
468 ob_start();
469 $moveUploadedFileResult = move_uploaded_file(
470 (string) $this->getName(),
471 $newFileToUpload,
473 ob_end_clean();
474 if (! $moveUploadedFileResult) {
475 $this->errorMessage = Message::error(__('Error while moving uploaded file.'));
477 return false;
480 $this->setName($newFileToUpload);
481 $this->isTemp(true);
483 if (! $this->isReadable()) {
484 $this->errorMessage = Message::error(__('Cannot read uploaded file.'));
486 return false;
489 return true;
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
505 ob_start();
506 $file = fopen((string) $this->getName(), 'rb');
507 ob_end_clean();
509 if (! $file) {
510 $this->errorMessage = Message::error(__('File could not be read!'));
512 return false;
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) {
538 $this->open();
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 '
563 . 'configuration.',
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()) {
579 case false:
580 return false;
582 case 'application/bzip2':
583 if (! $this->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 (! $this->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 ($this->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 ($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 return $this->compression ?? $this->detectCompression();
729 * Returns the offset
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 ?? '');