File.php 21 KB

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