PageRenderTime 81ms CodeModel.GetById 46ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/classes/File.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 837 lines | 436 code | 118 blank | 283 comment | 47 complexity | f5ea23cf178980fa8825a4eaa9206b86 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use ZipArchive;
  5. use function __;
  6. use function basename;
  7. use function bin2hex;
  8. use function bzopen;
  9. use function bzread;
  10. use function extension_loaded;
  11. use function fclose;
  12. use function feof;
  13. use function file_get_contents;
  14. use function fopen;
  15. use function fread;
  16. use function function_exists;
  17. use function gzopen;
  18. use function gzread;
  19. use function is_link;
  20. use function is_readable;
  21. use function is_string;
  22. use function is_uploaded_file;
  23. use function mb_strcut;
  24. use function move_uploaded_file;
  25. use function ob_end_clean;
  26. use function ob_start;
  27. use function sprintf;
  28. use function strlen;
  29. use function tempnam;
  30. use function trim;
  31. use function unlink;
  32. use const UPLOAD_ERR_CANT_WRITE;
  33. use const UPLOAD_ERR_EXTENSION;
  34. use const UPLOAD_ERR_FORM_SIZE;
  35. use const UPLOAD_ERR_INI_SIZE;
  36. use const UPLOAD_ERR_NO_FILE;
  37. use const UPLOAD_ERR_NO_TMP_DIR;
  38. use const UPLOAD_ERR_OK;
  39. use const UPLOAD_ERR_PARTIAL;
  40. /**
  41. * File wrapper class
  42. *
  43. * @todo when uploading a file into a blob field, should we also consider using
  44. * chunks like in import? UPDATE `table` SET `field` = `field` + [chunk]
  45. */
  46. class File
  47. {
  48. /**
  49. * @var string the temporary file name
  50. * @access protected
  51. */
  52. protected $name = null;
  53. /**
  54. * @var string the content
  55. * @access protected
  56. */
  57. protected $content = null;
  58. /**
  59. * @var Message|null the error message
  60. * @access protected
  61. */
  62. protected $errorMessage = null;
  63. /**
  64. * @var bool whether the file is temporary or not
  65. * @access protected
  66. */
  67. protected $isTemp = false;
  68. /**
  69. * @var string type of compression
  70. * @access protected
  71. */
  72. protected $compression = null;
  73. /** @var int */
  74. protected $offset = 0;
  75. /** @var int size of chunk to read with every step */
  76. protected $chunkSize = 32768;
  77. /** @var resource|null file handle */
  78. protected $handle = null;
  79. /** @var bool whether to decompress content before returning */
  80. protected $decompress = false;
  81. /** @var string charset of file */
  82. protected $charset = null;
  83. /** @var ZipExtension */
  84. private $zipExtension;
  85. /**
  86. * @param bool|string $name file name or false
  87. *
  88. * @access public
  89. */
  90. public function __construct($name = false)
  91. {
  92. if ($name && is_string($name)) {
  93. $this->setName($name);
  94. }
  95. if (! extension_loaded('zip')) {
  96. return;
  97. }
  98. $this->zipExtension = new ZipExtension(new ZipArchive());
  99. }
  100. /**
  101. * destructor
  102. *
  103. * @see File::cleanUp()
  104. *
  105. * @access public
  106. */
  107. public function __destruct()
  108. {
  109. $this->cleanUp();
  110. }
  111. /**
  112. * deletes file if it is temporary, usually from a moved upload file
  113. *
  114. * @access public
  115. */
  116. public function cleanUp(): bool
  117. {
  118. if ($this->isTemp()) {
  119. return $this->delete();
  120. }
  121. return true;
  122. }
  123. /**
  124. * deletes the file
  125. *
  126. * @access public
  127. */
  128. public function delete(): bool
  129. {
  130. return unlink((string) $this->getName());
  131. }
  132. /**
  133. * checks or sets the temp flag for this file
  134. * file objects with temp flags are deleted with object destruction
  135. *
  136. * @param bool $is_temp sets the temp flag
  137. *
  138. * @access public
  139. */
  140. public function isTemp(?bool $is_temp = null): bool
  141. {
  142. if ($is_temp !== null) {
  143. $this->isTemp = $is_temp;
  144. }
  145. return $this->isTemp;
  146. }
  147. /**
  148. * accessor
  149. *
  150. * @param string|null $name file name
  151. *
  152. * @access public
  153. */
  154. public function setName(?string $name): void
  155. {
  156. $this->name = trim((string) $name);
  157. }
  158. /**
  159. * Gets file content
  160. *
  161. * @return string|false the binary file content, or false if no content
  162. *
  163. * @access public
  164. */
  165. public function getRawContent()
  166. {
  167. if ($this->content !== null) {
  168. return $this->content;
  169. }
  170. if ($this->isUploaded() && ! $this->checkUploadedFile()) {
  171. return false;
  172. }
  173. if (! $this->isReadable()) {
  174. return false;
  175. }
  176. $this->content = file_get_contents((string) $this->getName());
  177. return $this->content;
  178. }
  179. /**
  180. * Gets file content
  181. *
  182. * @return string|false the binary file content as a string,
  183. * or false if no content
  184. *
  185. * @access public
  186. */
  187. public function getContent()
  188. {
  189. $result = $this->getRawContent();
  190. if ($result === false) {
  191. return false;
  192. }
  193. return '0x' . bin2hex($result);
  194. }
  195. /**
  196. * Whether file is uploaded.
  197. *
  198. * @access public
  199. */
  200. public function isUploaded(): bool
  201. {
  202. if ($this->getName() === null) {
  203. return false;
  204. }
  205. return is_uploaded_file($this->getName());
  206. }
  207. /**
  208. * accessor
  209. *
  210. * @return string|null File::$_name
  211. *
  212. * @access public
  213. */
  214. public function getName(): ?string
  215. {
  216. return $this->name;
  217. }
  218. /**
  219. * Initializes object from uploaded file.
  220. *
  221. * @param string $name name of file uploaded
  222. *
  223. * @access public
  224. */
  225. public function setUploadedFile(string $name): bool
  226. {
  227. $this->setName($name);
  228. if (! $this->isUploaded()) {
  229. $this->setName(null);
  230. $this->errorMessage = Message::error(__('File was not an uploaded file.'));
  231. return false;
  232. }
  233. return true;
  234. }
  235. /**
  236. * Loads uploaded file from table change request.
  237. *
  238. * @param string $key the md5 hash of the column name
  239. * @param string $rownumber number of row to process
  240. *
  241. * @access public
  242. */
  243. public function setUploadedFromTblChangeRequest(
  244. string $key,
  245. string $rownumber
  246. ): bool {
  247. if (
  248. ! isset($_FILES['fields_upload'])
  249. || empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])
  250. ) {
  251. return false;
  252. }
  253. $file = $this->fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
  254. switch ($file['error']) {
  255. case UPLOAD_ERR_OK:
  256. return $this->setUploadedFile($file['tmp_name']);
  257. case UPLOAD_ERR_NO_FILE:
  258. break;
  259. case UPLOAD_ERR_INI_SIZE:
  260. $this->errorMessage = Message::error(__(
  261. 'The uploaded file exceeds the upload_max_filesize directive in php.ini.'
  262. ));
  263. break;
  264. case UPLOAD_ERR_FORM_SIZE:
  265. $this->errorMessage = Message::error(__(
  266. 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'
  267. ));
  268. break;
  269. case UPLOAD_ERR_PARTIAL:
  270. $this->errorMessage = Message::error(__(
  271. 'The uploaded file was only partially uploaded.'
  272. ));
  273. break;
  274. case UPLOAD_ERR_NO_TMP_DIR:
  275. $this->errorMessage = Message::error(__('Missing a temporary folder.'));
  276. break;
  277. case UPLOAD_ERR_CANT_WRITE:
  278. $this->errorMessage = Message::error(__('Failed to write file to disk.'));
  279. break;
  280. case UPLOAD_ERR_EXTENSION:
  281. $this->errorMessage = Message::error(__('File upload stopped by extension.'));
  282. break;
  283. default:
  284. $this->errorMessage = Message::error(__('Unknown error in file upload.'));
  285. }
  286. return false;
  287. }
  288. /**
  289. * strips some dimension from the multi-dimensional array from $_FILES
  290. *
  291. * <code>
  292. * $file['name']['multi_edit'][$rownumber][$key] = [value]
  293. * $file['type']['multi_edit'][$rownumber][$key] = [value]
  294. * $file['size']['multi_edit'][$rownumber][$key] = [value]
  295. * $file['tmp_name']['multi_edit'][$rownumber][$key] = [value]
  296. * $file['error']['multi_edit'][$rownumber][$key] = [value]
  297. *
  298. * // becomes:
  299. *
  300. * $file['name'] = [value]
  301. * $file['type'] = [value]
  302. * $file['size'] = [value]
  303. * $file['tmp_name'] = [value]
  304. * $file['error'] = [value]
  305. * </code>
  306. *
  307. * @param array $file the array
  308. * @param string $rownumber number of row to process
  309. * @param string $key key to process
  310. *
  311. * @return array
  312. *
  313. * @access public
  314. * @static
  315. */
  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],
  327. ];
  328. }
  329. /**
  330. * sets the name if the file to the one selected in the tbl_change form
  331. *
  332. * @param string $key the md5 hash of the column name
  333. * @param string $rownumber number of row to process
  334. *
  335. * @access public
  336. */
  337. public function setSelectedFromTblChangeRequest(
  338. string $key,
  339. ?string $rownumber = null
  340. ): bool {
  341. if (
  342. ! empty($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
  343. && is_string($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
  344. ) {
  345. // ... whether with multiple rows ...
  346. return $this->setLocalSelectedFile($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key]);
  347. }
  348. return false;
  349. }
  350. /**
  351. * Returns possible error message.
  352. *
  353. * @return Message|null error message
  354. *
  355. * @access public
  356. */
  357. public function getError(): ?Message
  358. {
  359. return $this->errorMessage;
  360. }
  361. /**
  362. * Checks whether there was any error.
  363. *
  364. * @access public
  365. */
  366. public function isError(): bool
  367. {
  368. return $this->errorMessage !== null;
  369. }
  370. /**
  371. * checks the superglobals provided if the tbl_change form is submitted
  372. * and uses the submitted/selected file
  373. *
  374. * @param string $key the md5 hash of the column name
  375. * @param string $rownumber number of row to process
  376. *
  377. * @access public
  378. */
  379. public function checkTblChangeForm(string $key, string $rownumber): bool
  380. {
  381. if ($this->setUploadedFromTblChangeRequest($key, $rownumber)) {
  382. // well done ...
  383. $this->errorMessage = null;
  384. return true;
  385. }
  386. if ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
  387. // well done ...
  388. $this->errorMessage = null;
  389. return true;
  390. }
  391. // all failed, whether just no file uploaded/selected or an error
  392. return false;
  393. }
  394. /**
  395. * Sets named file to be read from UploadDir.
  396. *
  397. * @param string $name file name
  398. *
  399. * @access public
  400. */
  401. public function setLocalSelectedFile(string $name): bool
  402. {
  403. if (empty($GLOBALS['cfg']['UploadDir'])) {
  404. return false;
  405. }
  406. if (! is_string($GLOBALS['cfg']['UploadDir'])) {
  407. return false;
  408. }
  409. $this->setName(
  410. Util::userDir($GLOBALS['cfg']['UploadDir']) . Core::securePath($name)
  411. );
  412. if (@is_link((string) $this->getName())) {
  413. $this->errorMessage = Message::error(__('File is a symbolic link'));
  414. $this->setName(null);
  415. return false;
  416. }
  417. if (! $this->isReadable()) {
  418. $this->errorMessage = Message::error(__('File could not be read!'));
  419. $this->setName(null);
  420. return false;
  421. }
  422. return true;
  423. }
  424. /**
  425. * Checks whether file can be read.
  426. *
  427. * @access public
  428. */
  429. public function isReadable(): bool
  430. {
  431. // suppress warnings from being displayed, but not from being logged
  432. // any file access outside of open_basedir will issue a warning
  433. return @is_readable((string) $this->getName());
  434. }
  435. /**
  436. * If we are on a server with open_basedir, we must move the file
  437. * before opening it. The FAQ 1.11 explains how to create the "./tmp"
  438. * directory - if needed
  439. *
  440. * @todo move check of $cfg['TempDir'] into Config?
  441. * @access public
  442. */
  443. public function checkUploadedFile(): bool
  444. {
  445. if ($this->isReadable()) {
  446. return true;
  447. }
  448. $tmp_subdir = $GLOBALS['config']->getUploadTempDir();
  449. if ($tmp_subdir === null) {
  450. // cannot create directory or access, point user to FAQ 1.11
  451. $this->errorMessage = Message::error(__(
  452. 'Error moving the uploaded file, see [doc@faq1-11]FAQ 1.11[/doc].'
  453. ));
  454. return false;
  455. }
  456. $new_file_to_upload = (string) tempnam(
  457. $tmp_subdir,
  458. basename((string) $this->getName())
  459. );
  460. // suppress warnings from being displayed, but not from being logged
  461. // any file access outside of open_basedir will issue a warning
  462. ob_start();
  463. $move_uploaded_file_result = move_uploaded_file(
  464. (string) $this->getName(),
  465. $new_file_to_upload
  466. );
  467. ob_end_clean();
  468. if (! $move_uploaded_file_result) {
  469. $this->errorMessage = Message::error(__('Error while moving uploaded file.'));
  470. return false;
  471. }
  472. $this->setName($new_file_to_upload);
  473. $this->isTemp(true);
  474. if (! $this->isReadable()) {
  475. $this->errorMessage = Message::error(__('Cannot read uploaded file.'));
  476. return false;
  477. }
  478. return true;
  479. }
  480. /**
  481. * Detects what compression the file uses
  482. *
  483. * @return string|false false on error, otherwise string MIME type of
  484. * compression, none for none
  485. *
  486. * @todo move file read part into readChunk() or getChunk()
  487. * @todo add support for compression plugins
  488. * @access protected
  489. */
  490. protected function detectCompression()
  491. {
  492. // suppress warnings from being displayed, but not from being logged
  493. // f.e. any file access outside of open_basedir will issue a warning
  494. ob_start();
  495. $file = fopen((string) $this->getName(), 'rb');
  496. ob_end_clean();
  497. if (! $file) {
  498. $this->errorMessage = Message::error(__('File could not be read!'));
  499. return false;
  500. }
  501. $this->compression = Util::getCompressionMimeType($file);
  502. return $this->compression;
  503. }
  504. /**
  505. * Sets whether the content should be decompressed before returned
  506. *
  507. * @param bool $decompress whether to decompress
  508. */
  509. public function setDecompressContent(bool $decompress): void
  510. {
  511. $this->decompress = $decompress;
  512. }
  513. /**
  514. * Returns the file handle
  515. *
  516. * @return resource|null file handle
  517. */
  518. public function getHandle()
  519. {
  520. if ($this->handle === null) {
  521. $this->open();
  522. }
  523. return $this->handle;
  524. }
  525. /**
  526. * Sets the file handle
  527. *
  528. * @param resource $handle file handle
  529. */
  530. public function setHandle($handle): void
  531. {
  532. $this->handle = $handle;
  533. }
  534. /**
  535. * Sets error message for unsupported compression.
  536. */
  537. public function errorUnsupported(): void
  538. {
  539. $this->errorMessage = Message::error(sprintf(
  540. __(
  541. 'You attempted to load file with unsupported compression (%s). '
  542. . 'Either support for it is not implemented or disabled by your '
  543. . 'configuration.'
  544. ),
  545. $this->getCompression()
  546. ));
  547. }
  548. /**
  549. * Attempts to open the file.
  550. */
  551. public function open(): bool
  552. {
  553. if (! $this->decompress) {
  554. $this->handle = @fopen((string) $this->getName(), 'r');
  555. }
  556. switch ($this->getCompression()) {
  557. case false:
  558. return false;
  559. case 'application/bzip2':
  560. if (! $GLOBALS['cfg']['BZipDump'] || ! function_exists('bzopen')) {
  561. $this->errorUnsupported();
  562. return false;
  563. }
  564. $this->handle = @bzopen($this->getName(), 'r');
  565. break;
  566. case 'application/gzip':
  567. if (! $GLOBALS['cfg']['GZipDump'] || ! function_exists('gzopen')) {
  568. $this->errorUnsupported();
  569. return false;
  570. }
  571. $this->handle = @gzopen((string) $this->getName(), 'r');
  572. break;
  573. case 'application/zip':
  574. if ($GLOBALS['cfg']['ZipDump'] && function_exists('zip_open')) {
  575. return $this->openZip();
  576. }
  577. $this->errorUnsupported();
  578. return false;
  579. case 'none':
  580. $this->handle = @fopen((string) $this->getName(), 'r');
  581. break;
  582. default:
  583. $this->errorUnsupported();
  584. return false;
  585. }
  586. return $this->handle !== false;
  587. }
  588. /**
  589. * Opens file from zip
  590. *
  591. * @param string|null $specific_entry Entry to open
  592. */
  593. public function openZip(?string $specific_entry = null): bool
  594. {
  595. $result = $this->zipExtension->getContents($this->getName(), $specific_entry);
  596. if (! empty($result['error'])) {
  597. $this->errorMessage = Message::rawError($result['error']);
  598. return false;
  599. }
  600. $this->content = $result['data'];
  601. $this->offset = 0;
  602. return true;
  603. }
  604. /**
  605. * Checks whether we've reached end of file
  606. */
  607. public function eof(): bool
  608. {
  609. if ($this->handle !== null) {
  610. return feof($this->handle);
  611. }
  612. return $this->offset == strlen($this->content);
  613. }
  614. /**
  615. * Closes the file
  616. */
  617. public function close(): void
  618. {
  619. if ($this->handle !== null) {
  620. fclose($this->handle);
  621. $this->handle = null;
  622. } else {
  623. $this->content = '';
  624. $this->offset = 0;
  625. }
  626. $this->cleanUp();
  627. }
  628. /**
  629. * Reads data from file
  630. *
  631. * @param int $size Number of bytes to read
  632. */
  633. public function read(int $size): string
  634. {
  635. if ($this->compression === 'application/zip') {
  636. $result = mb_strcut($this->content, $this->offset, $size);
  637. $this->offset += strlen($result);
  638. return $result;
  639. }
  640. if ($this->handle === null) {
  641. return '';
  642. }
  643. if ($this->compression === 'application/bzip2') {
  644. return (string) bzread($this->handle, $size);
  645. }
  646. if ($this->compression === 'application/gzip') {
  647. return (string) gzread($this->handle, $size);
  648. }
  649. return (string) fread($this->handle, $size);
  650. }
  651. /**
  652. * Returns the character set of the file
  653. *
  654. * @return string character set of the file
  655. */
  656. public function getCharset(): string
  657. {
  658. return $this->charset;
  659. }
  660. /**
  661. * Sets the character set of the file
  662. *
  663. * @param string $charset character set of the file
  664. */
  665. public function setCharset(string $charset): void
  666. {
  667. $this->charset = $charset;
  668. }
  669. /**
  670. * Returns compression used by file.
  671. *
  672. * @return string MIME type of compression, none for none
  673. *
  674. * @access public
  675. */
  676. public function getCompression(): string
  677. {
  678. if ($this->compression === null) {
  679. return $this->detectCompression();
  680. }
  681. return $this->compression;
  682. }
  683. /**
  684. * Returns the offset
  685. *
  686. * @return int the offset
  687. */
  688. public function getOffset(): int
  689. {
  690. return $this->offset;
  691. }
  692. /**
  693. * Returns the chunk size
  694. *
  695. * @return int the chunk size
  696. */
  697. public function getChunkSize(): int
  698. {
  699. return $this->chunkSize;
  700. }
  701. /**
  702. * Sets the chunk size
  703. *
  704. * @param int $chunkSize the chunk size
  705. */
  706. public function setChunkSize(int $chunkSize): void
  707. {
  708. $this->chunkSize = $chunkSize;
  709. }
  710. /**
  711. * Returns the length of the content in the file
  712. *
  713. * @return int the length of the file content
  714. */
  715. public function getContentLength(): int
  716. {
  717. return strlen($this->content);
  718. }
  719. }