File.php 20 KB

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