File.php 21 KB

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