File.php 21 KB

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