Refactored ConfigFile class so that it is no longer a singleton
[phpmyadmin.git] / libraries / File.class.php
blobc65d659f91da4c9a14e757fc8d8100409de99b28
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * file upload functions
6 * @package PhpMyAdmin
7 */
8 if (! defined('PHPMYADMIN')) {
9 exit;
12 /**
13 * File wrapper class
15 * @todo when uploading a file into a blob field, should we also consider using
16 * chunks like in import? UPDATE `table` SET `field` = `field` + [chunk]
18 * @package PhpMyAdmin
20 class PMA_File
22 /**
23 * @var string the temporary file name
24 * @access protected
26 var $_name = null;
28 /**
29 * @var string the content
30 * @access protected
32 var $_content = null;
34 /**
35 * @var string the error message
36 * @access protected
38 var $_error_message = '';
40 /**
41 * @var bool whether the file is temporary or not
42 * @access protected
44 var $_is_temp = false;
46 /**
47 * @var string type of compression
48 * @access protected
50 var $_compression = null;
52 /**
53 * @var integer
55 var $_offset = 0;
57 /**
58 * @var integer size of chunk to read with every step
60 var $_chunk_size = 32768;
62 /**
63 * @var resource file handle
65 var $_handle = null;
67 /**
68 * @var boolean whether to decompress content before returning
70 var $_decompress = false;
72 /**
73 * @var string charset of file
75 var $_charset = null;
77 /**
78 * constructor
80 * @param string $name file name
82 * @access public
84 public function __construct($name = false)
86 if ($name) {
87 $this->setName($name);
91 /**
92 * destructor
94 * @see PMA_File::cleanUp()
95 * @access public
97 public function __destruct()
99 $this->cleanUp();
103 * deletes file if it is temporary, usually from a moved upload file
105 * @access public
106 * @return boolean success
108 public function cleanUp()
110 if ($this->isTemp()) {
111 return $this->delete();
114 return true;
118 * deletes the file
120 * @access public
121 * @return boolean success
123 public function delete()
125 return unlink($this->getName());
129 * checks or sets the temp flag for this file
130 * file objects with temp flags are deleted with object destruction
132 * @param boolean $is_temp sets the temp flag
134 * @return boolean PMA_File::$_is_temp
135 * @access public
137 public function isTemp($is_temp = null)
139 if (null !== $is_temp) {
140 $this->_is_temp = (bool) $is_temp;
143 return $this->_is_temp;
147 * accessor
149 * @param string $name file name
151 * @return void
152 * @access public
154 public function setName($name)
156 $this->_name = trim($name);
160 * Gets file content
162 * @param boolean $as_binary whether to return content as binary
163 * @param integer $offset starting offset
164 * @param integer $length length
166 * @return mixed the binary file content as a string,
167 * or false if no content
169 * @access public
171 public function getContent($as_binary = true, $offset = 0, $length = null)
173 if (null === $this->_content) {
174 if ($this->isUploaded() && ! $this->checkUploadedFile()) {
175 return false;
178 if (! $this->isReadable()) {
179 return false;
182 if (function_exists('file_get_contents')) {
183 $this->_content = file_get_contents($this->getName());
184 } elseif ($size = filesize($this->getName())) {
185 $this->_content = fread(fopen($this->getName(), 'rb'), $size);
189 if (! empty($this->_content) && $as_binary) {
190 return '0x' . bin2hex($this->_content);
193 if (null !== $length) {
194 return substr($this->_content, $offset, $length);
195 } elseif ($offset > 0) {
196 return substr($this->_content, $offset);
199 return $this->_content;
203 * Whether file is uploaded.
205 * @access public
207 * @return bool
209 public function isUploaded()
211 return is_uploaded_file($this->getName());
215 * accessor
217 * @access public
218 * @return string PMA_File::$_name
220 public function getName()
222 return $this->_name;
226 * Initializes object from uploaded file.
228 * @param string $name name of file uploaded
230 * @return boolean success
231 * @access public
233 public function setUploadedFile($name)
235 $this->setName($name);
237 if (! $this->isUploaded()) {
238 $this->setName(null);
239 $this->_error_message = __('File was not an uploaded file.');
240 return false;
243 return true;
247 * Loads uploaded file from table change request.
249 * @param string $key the md5 hash of the column name
250 * @param string $rownumber number of row to process
252 * @return boolean success
253 * @access public
255 public function setUploadedFromTblChangeRequest($key, $rownumber)
257 if (! isset($_FILES['fields_upload'])
258 || empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])
260 return false;
262 $file = PMA_File::fetchUploadedFromTblChangeRequestMultiple(
263 $_FILES['fields_upload'],
264 $rownumber,
265 $key
268 // check for file upload errors
269 switch ($file['error']) {
270 // we do not use the PHP constants here cause not all constants
271 // are defined in all versions of PHP - but the correct constants names
272 // are given as comment
273 case 0: //UPLOAD_ERR_OK:
274 return $this->setUploadedFile($file['tmp_name']);
275 break;
276 case 4: //UPLOAD_ERR_NO_FILE:
277 break;
278 case 1: //UPLOAD_ERR_INI_SIZE:
279 $this->_error_message = __('The uploaded file exceeds the upload_max_filesize directive in php.ini.');
280 break;
281 case 2: //UPLOAD_ERR_FORM_SIZE:
282 $this->_error_message = __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.');
283 break;
284 case 3: //UPLOAD_ERR_PARTIAL:
285 $this->_error_message = __('The uploaded file was only partially uploaded.');
286 break;
287 case 6: //UPLOAD_ERR_NO_TMP_DIR:
288 $this->_error_message = __('Missing a temporary folder.');
289 break;
290 case 7: //UPLOAD_ERR_CANT_WRITE:
291 $this->_error_message = __('Failed to write file to disk.');
292 break;
293 case 8: //UPLOAD_ERR_EXTENSION:
294 $this->_error_message = __('File upload stopped by extension.');
295 break;
296 default:
297 $this->_error_message = __('Unknown error in file upload.');
298 } // end switch
300 return false;
304 * strips some dimension from the multi-dimensional array from $_FILES
306 * <code>
307 * $file['name']['multi_edit'][$rownumber][$key] = [value]
308 * $file['type']['multi_edit'][$rownumber][$key] = [value]
309 * $file['size']['multi_edit'][$rownumber][$key] = [value]
310 * $file['tmp_name']['multi_edit'][$rownumber][$key] = [value]
311 * $file['error']['multi_edit'][$rownumber][$key] = [value]
313 * // becomes:
315 * $file['name'] = [value]
316 * $file['type'] = [value]
317 * $file['size'] = [value]
318 * $file['tmp_name'] = [value]
319 * $file['error'] = [value]
320 * </code>
322 * @param array $file the array
323 * @param string $rownumber number of row to process
324 * @param string $key key to process
326 * @return array
327 * @access public
328 * @static
330 public function fetchUploadedFromTblChangeRequestMultiple(
331 $file, $rownumber, $key
333 $new_file = array(
334 'name' => $file['name']['multi_edit'][$rownumber][$key],
335 'type' => $file['type']['multi_edit'][$rownumber][$key],
336 'size' => $file['size']['multi_edit'][$rownumber][$key],
337 'tmp_name' => $file['tmp_name']['multi_edit'][$rownumber][$key],
338 'error' => $file['error']['multi_edit'][$rownumber][$key],
341 return $new_file;
345 * sets the name if the file to the one selected in the tbl_change form
347 * @param string $key the md5 hash of the column name
348 * @param string $rownumber number of row to process
350 * @return boolean success
351 * @access public
353 public function setSelectedFromTblChangeRequest($key, $rownumber = null)
355 if (! empty($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
356 && is_string($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
358 // ... whether with multiple rows ...
359 return $this->setLocalSelectedFile(
360 $_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key]
362 } else {
363 return false;
368 * Returns possible error message.
370 * @access public
371 * @return string error message
373 public function getError()
375 return $this->_error_message;
379 * Checks whether there was any error.
381 * @access public
382 * @return boolean whether an error occurred or not
384 public function isError()
386 return ! empty($this->_error_message);
390 * checks the superglobals provided if the tbl_change form is submitted
391 * and uses the submitted/selected file
393 * @param string $key the md5 hash of the column name
394 * @param string $rownumber number of row to process
396 * @return boolean success
397 * @access public
399 public function checkTblChangeForm($key, $rownumber)
401 if ($this->setUploadedFromTblChangeRequest($key, $rownumber)) {
402 // well done ...
403 $this->_error_message = '';
404 return true;
405 } elseif ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
406 // well done ...
407 $this->_error_message = '';
408 return true;
410 // all failed, whether just no file uploaded/selected or an error
412 return false;
416 * Sets named file to be read from UploadDir.
418 * @param string $name file name
420 * @return boolean success
421 * @access public
423 public function setLocalSelectedFile($name)
425 if (empty($GLOBALS['cfg']['UploadDir'])) {
426 return false;
429 $this->setName(
430 PMA_Util::userDir($GLOBALS['cfg']['UploadDir']) . PMA_securePath($name)
432 if (! $this->isReadable()) {
433 $this->_error_message = __('File could not be read');
434 $this->setName(null);
435 return false;
438 return true;
442 * Checks whether file can be read.
444 * @access public
445 * @return boolean whether the file is readable or not
447 public function isReadable()
449 // suppress warnings from being displayed, but not from being logged
450 // any file access outside of open_basedir will issue a warning
451 ob_start();
452 $is_readable = is_readable($this->getName());
453 ob_end_clean();
454 return $is_readable;
458 * If we are on a server with open_basedir, we must move the file
459 * before opening it. The FAQ 1.11 explains how to create the "./tmp"
460 * directory - if needed
462 * @todo move check of $cfg['TempDir'] into PMA_Config?
463 * @access public
464 * @return boolean whether uploaded fiel is fine or not
466 public function checkUploadedFile()
468 if ($this->isReadable()) {
469 return true;
472 if (empty($GLOBALS['cfg']['TempDir'])
473 || ! is_writable($GLOBALS['cfg']['TempDir'])
475 // cannot create directory or access, point user to FAQ 1.11
476 $this->_error_message = __('Error moving the uploaded file, see [doc@faq1-11]FAQ 1.11[/doc]');
477 return false;
480 $new_file_to_upload = tempnam(
481 realpath($GLOBALS['cfg']['TempDir']),
482 basename($this->getName())
485 // suppress warnings from being displayed, but not from being logged
486 // any file access outside of open_basedir will issue a warning
487 ob_start();
488 $move_uploaded_file_result = move_uploaded_file(
489 $this->getName(),
490 $new_file_to_upload
492 ob_end_clean();
493 if (! $move_uploaded_file_result) {
494 $this->_error_message = __('Error while moving uploaded file.');
495 return false;
498 $this->setName($new_file_to_upload);
499 $this->isTemp(true);
501 if (! $this->isReadable()) {
502 $this->_error_message = __('Cannot read (moved) upload file.');
503 return false;
506 return true;
510 * Detects what compression filse uses
512 * @todo move file read part into readChunk() or getChunk()
513 * @todo add support for compression plugins
514 * @access protected
515 * @return string MIME type of compression, none for none
517 protected function detectCompression()
519 // suppress warnings from being displayed, but not from being logged
520 // f.e. any file access outside of open_basedir will issue a warning
521 ob_start();
522 $file = fopen($this->getName(), 'rb');
523 ob_end_clean();
525 if (! $file) {
526 $this->_error_message = __('File could not be read');
527 return false;
531 * @todo
532 * get registered plugins for file compression
534 foreach (PMA_getPlugins($type = 'compression') as $plugin) {
535 if ($plugin['classname']::canHandle($this->getName())) {
536 $this->setCompressionPlugin($plugin);
537 break;
542 $test = fread($file, 4);
543 $len = strlen($test);
544 fclose($file);
546 if ($len >= 2 && $test[0] == chr(31) && $test[1] == chr(139)) {
547 $this->_compression = 'application/gzip';
548 } elseif ($len >= 3 && substr($test, 0, 3) == 'BZh') {
549 $this->_compression = 'application/bzip2';
550 } elseif ($len >= 4 && $test == "PK\003\004") {
551 $this->_compression = 'application/zip';
552 } else {
553 $this->_compression = 'none';
556 return $this->_compression;
560 * Sets whether the content should be decompressed before returned
562 * @param boolean $decompress whether to decompres
564 * @return void
566 public function setDecompressContent($decompress)
568 $this->_decompress = (bool) $decompress;
572 * Returns the file handle
574 * @return object file handle
576 public function getHandle()
578 if (null === $this->_handle) {
579 $this->open();
581 return $this->_handle;
585 * Sets the file handle
587 * @param object $handle file handle
589 * @return void
591 public function setHandle($handle)
593 $this->_handle = $handle;
598 * Sets error message for unsupported compression.
600 * @return void
602 public function errorUnsupported()
604 $this->_error_message = sprintf(
605 __('You attempted to load file with unsupported compression (%s). Either support for it is not implemented or disabled by your configuration.'),
606 $this->getCompression()
611 * Attempts to open the file.
613 * @return bool
615 public function open()
617 if (! $this->_decompress) {
618 $this->_handle = @fopen($this->getName(), 'r');
621 switch ($this->getCompression()) {
622 case false:
623 return false;
624 case 'application/bzip2':
625 if ($GLOBALS['cfg']['BZipDump'] && @function_exists('bzopen')) {
626 $this->_handle = @bzopen($this->getName(), 'r');
627 } else {
628 $this->errorUnsupported();
629 return false;
631 break;
632 case 'application/gzip':
633 if ($GLOBALS['cfg']['GZipDump'] && @function_exists('gzopen')) {
634 $this->_handle = @gzopen($this->getName(), 'r');
635 } else {
636 $this->errorUnsupported();
637 return false;
639 break;
640 case 'application/zip':
641 if ($GLOBALS['cfg']['ZipDump'] && @function_exists('zip_open')) {
642 include_once './libraries/zip_extension.lib.php';
643 $result = PMA_getZipContents($this->getName());
644 if (! empty($result['error'])) {
645 $this->_error_message = PMA_Message::rawError($result['error']);
646 return false;
647 } else {
648 $this->content_uncompressed = $result['data'];
650 unset($result);
651 } else {
652 $this->errorUnsupported();
653 return false;
655 break;
656 case 'none':
657 $this->_handle = @fopen($this->getName(), 'r');
658 break;
659 default:
660 $this->errorUnsupported();
661 return false;
662 break;
665 return true;
669 * Returns the character set of the file
671 * @return string character set of the file
673 public function getCharset()
675 return $this->_charset;
679 * Sets the character set of the file
681 * @param string $charset character set of the file
683 * @return void
685 public function setCharset($charset)
687 $this->_charset = $charset;
691 * Returns compression used by file.
693 * @return string MIME type of compression, none for none
694 * @access public
696 public function getCompression()
698 if (null === $this->_compression) {
699 return $this->detectCompression();
702 return $this->_compression;
706 * advances the file pointer in the file handle by $length bytes/chars
708 * @param integer $length numbers of chars/bytes to skip
710 * @return boolean
711 * @todo this function is unused
713 public function advanceFilePointer($length)
715 while ($length > 0) {
716 $this->getNextChunk($length);
717 $length -= $this->getChunkSize();
722 * http://bugs.php.net/bug.php?id=29532
723 * bzip reads a maximum of 8192 bytes on windows systems
725 * @param int $max_size maximum size of the next chunk to be returned
727 * @return bool|string
728 * @todo this function is unused
730 public function getNextChunk($max_size = null)
732 if (null !== $max_size) {
733 $size = min($max_size, $this->getChunkSize());
734 } else {
735 $size = $this->getChunkSize();
738 // $result = $this->handler->getNextChunk($size);
739 $result = '';
740 switch ($this->getCompression()) {
741 case 'application/bzip2':
742 $result = '';
743 while (strlen($result) < $size - 8192 && ! feof($this->getHandle())) {
744 $result .= bzread($this->getHandle(), $size);
746 break;
747 case 'application/gzip':
748 $result = gzread($this->getHandle(), $size);
749 break;
750 case 'application/zip':
752 * if getNextChunk() is used some day,
753 * replace this code by code similar to the one
754 * in open()
756 include_once './libraries/unzip.lib.php';
757 $import_handle = new SimpleUnzip();
758 $import_handle->ReadFile($this->getName());
759 if ($import_handle->Count() == 0) {
760 $this->_error_message = __('No files found inside ZIP archive!');
761 return false;
762 } elseif ($import_handle->GetError(0) != 0) {
763 $this->_error_message = __('Error in ZIP archive:')
764 . ' ' . $import_handle->GetErrorMsg(0);
765 return false;
766 } else {
767 $result = $import_handle->GetData(0);
770 break;
771 case 'none':
772 $result = fread($this->getHandle(), $size);
773 break;
774 default:
775 return false;
778 if ($GLOBALS['charset_conversion']) {
779 $result = PMA_convertString($this->getCharset(), 'utf-8', $result);
780 } else {
782 * Skip possible byte order marks (I do not think we need more
783 * charsets, but feel free to add more, you can use wikipedia for
784 * reference: <http://en.wikipedia.org/wiki/Byte_Order_Mark>)
786 * @todo BOM could be used for charset autodetection
788 if ($this->getOffset() === 0) {
789 // UTF-8
790 if (strncmp($result, "\xEF\xBB\xBF", 3) == 0) {
791 $result = substr($result, 3);
792 // UTF-16 BE, LE
793 } elseif (strncmp($result, "\xFE\xFF", 2) == 0
794 || strncmp($result, "\xFF\xFE", 2) == 0
796 $result = substr($result, 2);
801 $this->_offset += $size;
802 if (0 === $result) {
803 return true;
805 return $result;
809 * Returns the offset
811 * @return integer the offset
813 public function getOffset()
815 return $this->_offset;
819 * Returns the chunk size
821 * @return integer the chunk size
823 public function getChunkSize()
825 return $this->_chunk_size;
829 * Sets the chunk size
831 * @param integer $chunk_size the chunk size
833 * @return void
835 public function setChunkSize($chunk_size)
837 $this->_chunk_size = (int) $chunk_size;
841 * Returns the length of the content in the file
843 * @return integer the length of the file content
845 public function getContentLength()
847 return strlen($this->_content);
851 * Returns whether the end of the file has been reached
853 * @return boolean whether the end of the file has been reached
855 public function eof()
857 if ($this->getHandle()) {
858 return feof($this->getHandle());
859 } else {
860 return ($this->getOffset() >= $this->getContentLength());