Merge remote-tracking branch 'origin/master' into drizzle
[phpmyadmin.git] / libraries / File.class.php
blobc2d5dad4b5aff4eb2608a6d868fe1ae9547b0a55
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * file upload functions
6 * @package phpMyAdmin
7 */
9 /**
11 * @todo replace error messages with localized string
12 * @todo when uploading a file into a blob field, should we also consider using
13 * chunks like in import? UPDATE `table` SET `field` = `field` + [chunk]
14 * @package phpMyAdmin
16 class PMA_File
18 /**
19 * @var string the temporary file name
20 * @access protected
22 var $_name = null;
24 /**
25 * @var string the content
26 * @access protected
28 var $_content = null;
30 /**
31 * @var string the error message
32 * @access protected
34 var $_error_message = '';
36 /**
37 * @var bool whether the file is temporary or not
38 * @access protected
40 var $_is_temp = false;
42 /**
43 * @var string type of compression
44 * @access protected
46 var $_compression = null;
48 /**
49 * @var integer
51 var $_offset = 0;
53 /**
54 * @var integer size of chunk to read with every step
56 var $_chunk_size = 32768;
58 /**
59 * @var resource file handle
61 var $_handle = null;
63 /**
64 * @var boolean whether to decompress content before returning
66 var $_decompress = false;
68 /**
69 * @var string charset of file
71 var $_charset = null;
73 /**
74 * @staticvar string most recent BLOB repository reference
76 static $_recent_bs_reference = NULL;
78 /**
79 * constructor
81 * @access public
82 * @param string $name file name
84 function __construct($name = false)
86 if ($name) {
87 $this->setName($name);
91 /**
92 * destructor
94 * @see PMA_File::cleanUp()
95 * @access public
97 function __destruct()
99 $this->cleanUp();
103 * deletes file if it is temporary, usally from a moved upload file
105 * @access public
106 * @return boolean success
108 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 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 * @access public
133 * @param boolean sets the temp flag
134 * @return boolean PMA_File::$_is_temp
136 function isTemp($is_temp = null)
138 if (null !== $is_temp) {
139 $this->_is_temp = (bool) $is_temp;
142 return $this->_is_temp;
146 * accessor
148 * @access public
149 * @param string $name file name
151 function setName($name)
153 $this->_name = trim($name);
157 * @access public
158 * @return string binary file content
160 function getContent($as_binary = true, $offset = 0, $length = null)
162 if (null === $this->_content) {
163 if ($this->isUploaded() && ! $this->checkUploadedFile()) {
164 return false;
167 if (! $this->isReadable()) {
168 return false;
171 if (function_exists('file_get_contents')) {
172 $this->_content = file_get_contents($this->getName());
173 } elseif ($size = filesize($this->getName())) {
174 $this->_content = fread(fopen($this->getName(), 'rb'), $size);
178 if (! empty($this->_content) && $as_binary) {
179 return '0x' . bin2hex($this->_content);
182 if (null !== $length) {
183 return substr($this->_content, $offset, $length);
184 } elseif ($offset > 0) {
185 return substr($this->_content, $offset);
188 return $this->_content;
192 * @access public
194 function isUploaded()
196 return is_uploaded_file($this->getName());
200 * accessor
202 * @access public
203 * @return string PMA_File::$_name
205 function getName()
207 return $this->_name;
211 * @todo replace error message with localized string
212 * @access public
213 * @param string name of file uploaded
214 * @return boolean success
216 function setUploadedFile($name)
218 $this->setName($name);
220 if (! $this->isUploaded()) {
221 $this->setName(null);
222 $this->_error_message = 'not an uploaded file';
223 return false;
226 return true;
230 * @access public
231 * @param string $key the md5 hash of the column name
232 * @param string $rownumber
233 * @return boolean success
235 function setUploadedFromTblChangeRequest($key, $rownumber)
237 if (! isset($_FILES['fields_upload']) || empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])) {
238 return false;
240 $file = PMA_File::fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
242 // for blobstreaming
243 $is_bs_upload = false;
245 // check if this field requires a repository upload
246 if (isset($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key])) {
247 $is_bs_upload = ($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key] == "on") ? true : false;
249 // if request is an upload to the BLOB repository
250 if ($is_bs_upload) {
251 $bs_db = $_REQUEST['db'];
252 $bs_table = $_REQUEST['table'];
253 $tmp_filename = $file['tmp_name'];
254 $tmp_file_type = $file['type'];
256 if (! $tmp_file_type) {
257 $tmp_file_type = NULL;
260 if (! $bs_db || ! $bs_table) {
261 $this->_error_message = $GLOBALS['strUploadErrorUnknown'];
262 return false;
264 $blob_url = PMA_BS_UpLoadFile($bs_db, $bs_table, $tmp_file_type, $tmp_filename);
265 PMA_File::setRecentBLOBReference($blob_url);
266 } // end if ($is_bs_upload)
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 * @todo re-check if requirements changes to PHP >= 4.2.0
323 * @access public
324 * @static
325 * @param array $file the array
326 * @param string $rownumber
327 * @param string $key
328 * @return array
330 function fetchUploadedFromTblChangeRequestMultiple($file, $rownumber, $key)
332 $new_file = array(
333 'name' => $file['name']['multi_edit'][$rownumber][$key],
334 'type' => $file['type']['multi_edit'][$rownumber][$key],
335 'size' => $file['size']['multi_edit'][$rownumber][$key],
336 'tmp_name' => $file['tmp_name']['multi_edit'][$rownumber][$key],
337 'error' => $file['error']['multi_edit'][$rownumber][$key],
340 return $new_file;
344 * sets the name if the file to the one selected in the tbl_change form
346 * @access public
347 * @param string $key the md5 hash of the column name
348 * @param string $rownumber
349 * @return boolean success
351 function setSelectedFromTblChangeRequest($key, $rownumber = null)
353 if (! empty($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
354 && is_string($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])) {
355 // ... whether with multiple rows ...
356 // for blobstreaming
357 $is_bs_upload = false;
359 // check if this field requires a repository upload
360 if (isset($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key])) {
361 $is_bs_upload = ($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key] == "on") ? true : false;
364 // is a request to upload file to BLOB repository using uploadDir mechanism
365 if ($is_bs_upload) {
366 $bs_db = $_REQUEST['db'];
367 $bs_table = $_REQUEST['table'];
368 $tmp_filename = $GLOBALS['cfg']['UploadDir'] . '/' . $_REQUEST['fields_uploadlocal_' . $key]['multi_edit'][$rownumber];
370 // check if fileinfo library exists
371 if ($PMA_Config->get('FILEINFO_EXISTS')) {
372 // attempt to init fileinfo
373 $finfo = finfo_open(FILEINFO_MIME);
375 // fileinfo exists
376 if ($finfo) {
377 // pass in filename to fileinfo and close fileinfo handle after
378 $tmp_file_type = finfo_file($finfo, $tmp_filename);
379 finfo_close($finfo);
381 } else {
382 // no fileinfo library exists, use file command
383 $tmp_file_type = exec("file -bi " . escapeshellarg($tmp_filename));
386 if (! $tmp_file_type) {
387 $tmp_file_type = NULL;
390 if (! $bs_db || !$bs_table) {
391 $this->_error_message = $GLOBALS['strUploadErrorUnknown'];
392 return false;
394 $blob_url = PMA_BS_UpLoadFile($bs_db, $bs_table, $tmp_file_type, $tmp_filename);
395 PMA_File::setRecentBLOBReference($blob_url);
396 } // end if ($is_bs_upload)
398 return $this->setLocalSelectedFile($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key]);
399 } else {
400 return false;
405 * @access public
406 * @return string error message
408 function getError()
410 return $this->_error_message;
414 * @access public
415 * @return boolean whether an error occured or not
417 function isError()
419 return ! empty($this->_error_message);
423 * checks the superglobals provided if the tbl_change form is submitted
424 * and uses the submitted/selected file
426 * @access public
427 * @param string $key the md5 hash of the column name
428 * @param string $rownumber
429 * @return boolean success
431 function checkTblChangeForm($key, $rownumber)
433 if ($this->setUploadedFromTblChangeRequest($key, $rownumber)) {
434 // well done ...
435 $this->_error_message = '';
436 return true;
437 } elseif ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
438 // well done ...
439 $this->_error_message = '';
440 return true;
442 // all failed, whether just no file uploaded/selected or an error
444 return false;
449 * @access public
450 * @param string $name
451 * @return boolean success
453 function setLocalSelectedFile($name)
455 if (empty($GLOBALS['cfg']['UploadDir'])) return false;
457 $this->setName(PMA_userDir($GLOBALS['cfg']['UploadDir']) . PMA_securePath($name));
458 if (! $this->isReadable()) {
459 $this->_error_message = __('File could not be read');
460 $this->setName(null);
461 return false;
464 return true;
468 * @access public
469 * @return boolean whether the file is readable or not
471 function isReadable()
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 $is_readable = is_readable($this->getName());
477 ob_end_clean();
478 return $is_readable;
482 * If we are on a server with open_basedir, we must move the file
483 * before opening it. The FAQ 1.11 explains how to create the "./tmp"
484 * directory - if needed
486 * @todo replace error message with localized string
487 * @todo move check of $cfg['TempDir'] into PMA_Config?
488 * @access public
489 * @return boolean whether uploaded fiel is fine or not
491 function checkUploadedFile()
493 if ($this->isReadable()) {
494 return true;
497 if (empty($GLOBALS['cfg']['TempDir']) || ! is_writable($GLOBALS['cfg']['TempDir'])) {
498 // cannot create directory or access, point user to FAQ 1.11
499 $this->_error_message = __('Error moving the uploaded file, see [a@./Documentation.html#faq1_11@Documentation]FAQ 1.11[/a]');
500 return false;
503 $new_file_to_upload = tempnam(realpath($GLOBALS['cfg']['TempDir']), basename($this->getName()));
505 // suppress warnings from being displayed, but not from being logged
506 // any file access outside of open_basedir will issue a warning
507 ob_start();
508 $move_uploaded_file_result = move_uploaded_file($this->getName(), $new_file_to_upload);
509 ob_end_clean();
510 if (! $move_uploaded_file_result) {
511 $this->_error_message = 'error while moving uploaded file';
512 return false;
515 $this->setName($new_file_to_upload);
516 $this->isTemp(true);
518 if (! $this->isReadable()) {
519 $this->_error_message = 'cannot read (moved) upload file';
520 return false;
523 return true;
527 * Detects what compression filse uses
529 * @todo move file read part into readChunk() or getChunk()
530 * @todo add support for compression plugins
531 * @access protected
532 * @return string MIME type of compression, none for none
534 function _detectCompression()
536 // suppress warnings from being displayed, but not from being logged
537 // f.e. any file access outside of open_basedir will issue a warning
538 ob_start();
539 $file = fopen($this->getName(), 'rb');
540 ob_end_clean();
542 if (! $file) {
543 $this->_error_message = __('File could not be read');
544 return false;
548 * @todo
549 * get registered plugins for file compression
551 foreach (PMA_getPlugins($type = 'compression') as $plugin) {
552 if (call_user_func_array(array($plugin['classname'], 'canHandle'), array($this->getName()))) {
553 $this->setCompressionPlugin($plugin);
554 break;
559 $test = fread($file, 4);
560 $len = strlen($test);
561 fclose($file);
563 if ($len >= 2 && $test[0] == chr(31) && $test[1] == chr(139)) {
564 $this->_compression = 'application/gzip';
565 } elseif ($len >= 3 && substr($test, 0, 3) == 'BZh') {
566 $this->_compression = 'application/bzip2';
567 } elseif ($len >= 4 && $test == "PK\003\004") {
568 $this->_compression = 'application/zip';
569 } else {
570 $this->_compression = 'none';
573 return $this->_compression;
577 * whether the content should be decompressed before returned
579 function setDecompressContent($decompress)
581 $this->_decompress = (bool) $decompress;
584 function getHandle()
586 if (null === $this->_handle) {
587 $this->open();
589 return $this->_handle;
592 function setHandle($handle)
594 $this->_handle = $handle;
600 function open()
602 if (! $this->_decompress) {
603 $this->_handle = @fopen($this->getName(), 'r');
606 switch ($this->getCompression()) {
607 case false:
608 return false;
609 case 'application/bzip2':
610 if ($GLOBALS['cfg']['BZipDump'] && @function_exists('bzopen')) {
611 $this->_handle = @bzopen($this->getName(), 'r');
612 } else {
613 $this->_error_message = sprintf(__('You attempted to load file with unsupported compression (%s). Either support for it is not implemented or disabled by your configuration.'), $this->getCompression());
614 return false;
616 break;
617 case 'application/gzip':
618 if ($GLOBALS['cfg']['GZipDump'] && @function_exists('gzopen')) {
619 $this->_handle = @gzopen($this->getName(), 'r');
620 } else {
621 $this->_error_message = sprintf(__('You attempted to load file with unsupported compression (%s). Either support for it is not implemented or disabled by your configuration.'), $this->getCompression());
622 return false;
624 break;
625 case 'application/zip':
626 if ($GLOBALS['cfg']['ZipDump'] && @function_exists('zip_open')) {
627 include_once './libraries/zip_extension.lib.php';
628 $result = PMA_getZipContents($this->getName());
629 if (! empty($result['error'])) {
630 $this->_error_message = PMA_Message::rawError($result['error']);
631 return false;
632 } else {
633 $this->content_uncompressed = $result['data'];
635 unset($result);
636 } else {
637 $this->_error_message = sprintf(__('You attempted to load file with unsupported compression (%s). Either support for it is not implemented or disabled by your configuration.'), $this->getCompression());
638 return false;
640 break;
641 case 'none':
642 $this->_handle = @fopen($this->getName(), 'r');
643 break;
644 default:
645 $this->_error_message = sprintf(__('You attempted to load file with unsupported compression (%s). Either support for it is not implemented or disabled by your configuration.'), $this->getCompression());
646 return false;
647 break;
653 function getCharset()
655 return $this->_charset;
658 function setCharset($charset)
660 $this->_charset = $charset;
664 * @return string MIME type of compression, none for none
665 * @access public
667 function getCompression()
669 if (null === $this->_compression) {
670 return $this->_detectCompression();
673 return $this->_compression;
677 * advances the file pointer in the file handle by $length bytes/chars
679 * @param integer $length numbers of chars/bytes to skip
680 * @return boolean
681 * @todo this function is unused
683 function advanceFilePointer($length)
685 while ($length > 0) {
686 // Disable read progresivity, otherwise we eat all memory!
687 $read_multiply = 1; // required?
688 $this->getNextChunk($length);
689 $length -= $this->getChunkSize();
694 * http://bugs.php.net/bug.php?id=29532
695 * bzip reads a maximum of 8192 bytes on windows systems
696 * @todo this function is unused
698 function getNextChunk($max_size = null)
700 if (null !== $max_size) {
701 $size = min($max_size, $this->getChunkSize());
702 } else {
703 $size = $this->getChunkSize();
706 // $result = $this->handler->getNextChunk($size);
707 $result = '';
708 switch ($this->getCompression()) {
709 case 'application/bzip2':
710 $result = '';
711 while (strlen($result) < $size - 8192 && ! feof($this->getHandle())) {
712 $result .= bzread($this->getHandle(), $size);
714 break;
715 case 'application/gzip':
716 $result = gzread($this->getHandle(), $size);
717 break;
718 case 'application/zip':
720 * if getNextChunk() is used some day,
721 * replace this code by code similar to the one
722 * in open()
724 include_once './libraries/unzip.lib.php';
725 $import_handle = new SimpleUnzip();
726 $import_handle->ReadFile($this->getName());
727 if ($import_handle->Count() == 0) {
728 $this->_error_message = __('No files found inside ZIP archive!');
729 return false;
730 } elseif ($import_handle->GetError(0) != 0) {
731 $this->_error_message = __('Error in ZIP archive:')
732 . ' ' . $import_handle->GetErrorMsg(0);
733 return false;
734 } else {
735 $result = $import_handle->GetData(0);
738 break;
739 case 'none':
740 $result = fread($this->getHandle(), $size);
741 break;
742 default:
743 return false;
746 echo $size . ' - ';
747 echo strlen($result) . ' - ';
748 echo (@$GLOBALS['__len__'] += strlen($result)) . ' - ';
749 echo $this->_error_message;
750 echo '<hr />';
752 if ($GLOBALS['charset_conversion']) {
753 $result = PMA_convert_string($this->getCharset(), 'utf-8', $result);
754 } else {
756 * Skip possible byte order marks (I do not think we need more
757 * charsets, but feel free to add more, you can use wikipedia for
758 * reference: <http://en.wikipedia.org/wiki/Byte_Order_Mark>)
760 * @todo BOM could be used for charset autodetection
762 if ($this->getOffset() === 0) {
763 // UTF-8
764 if (strncmp($result, "\xEF\xBB\xBF", 3) == 0) {
765 $result = substr($result, 3);
766 // UTF-16 BE, LE
767 } elseif (strncmp($result, "\xFE\xFF", 2) == 0
768 || strncmp($result, "\xFF\xFE", 2) == 0) {
769 $result = substr($result, 2);
774 $this->_offset += $size;
775 if (0 === $result) {
776 return true;
778 return $result;
781 function getOffset()
783 return $this->_offset;
786 function getChunkSize()
788 return $this->_chunk_size;
791 function setChunkSize($chunk_size)
793 $this->_chunk_size = (int) $chunk_size;
796 function getContentLength()
798 return strlen($this->_content);
801 function eof()
803 if ($this->getHandle()) {
804 return feof($this->getHandle());
805 } else {
806 return ($this->getOffset() >= $this->getContentLength());
812 * sets reference to most recent BLOB repository reference
814 * @access public
815 * @param string - BLOB repository reference
817 static function setRecentBLOBReference($ref)
819 PMA_File::$_recent_bs_reference = $ref;
823 * retrieves reference to most recent BLOB repository reference
825 * @access public
826 * @return string - most recent BLOB repository reference
828 static function getRecentBLOBReference()
830 $ref = PMA_File::$_recent_bs_reference;
831 PMA_File::$_recent_bs_reference = NULL;
833 return $ref;