Translated using Weblate.
[phpmyadmin.git] / libraries / File.class.php
blob3745866929b20b9035290e83d8d4ca9cea0ebf15
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * file upload functions
6 * @package PhpMyAdmin
7 */
9 /**
11 * @todo when uploading a file into a blob field, should we also consider using
12 * chunks like in import? UPDATE `table` SET `field` = `field` + [chunk]
13 * @package PhpMyAdmin
15 class PMA_File
17 /**
18 * @var string the temporary file name
19 * @access protected
21 var $_name = null;
23 /**
24 * @var string the content
25 * @access protected
27 var $_content = null;
29 /**
30 * @var string the error message
31 * @access protected
33 var $_error_message = '';
35 /**
36 * @var bool whether the file is temporary or not
37 * @access protected
39 var $_is_temp = false;
41 /**
42 * @var string type of compression
43 * @access protected
45 var $_compression = null;
47 /**
48 * @var integer
50 var $_offset = 0;
52 /**
53 * @var integer size of chunk to read with every step
55 var $_chunk_size = 32768;
57 /**
58 * @var resource file handle
60 var $_handle = null;
62 /**
63 * @var boolean whether to decompress content before returning
65 var $_decompress = false;
67 /**
68 * @var string charset of file
70 var $_charset = null;
72 /**
73 * @staticvar string most recent BLOB repository reference
75 static $_recent_bs_reference = null;
77 /**
78 * constructor
80 * @access public
81 * @param string $name file name
83 function __construct($name = false)
85 if ($name) {
86 $this->setName($name);
90 /**
91 * destructor
93 * @see PMA_File::cleanUp()
94 * @access public
96 function __destruct()
98 $this->cleanUp();
102 * deletes file if it is temporary, usally from a moved upload file
104 * @access public
105 * @return boolean success
107 function cleanUp()
109 if ($this->isTemp()) {
110 return $this->delete();
113 return true;
117 * deletes the file
119 * @access public
120 * @return boolean success
122 function delete()
124 return unlink($this->getName());
128 * checks or sets the temp flag for this file
129 * file objects with temp flags are deleted with object destruction
131 * @access public
132 * @param boolean sets the temp flag
133 * @return boolean PMA_File::$_is_temp
135 function isTemp($is_temp = null)
137 if (null !== $is_temp) {
138 $this->_is_temp = (bool) $is_temp;
141 return $this->_is_temp;
145 * accessor
147 * @access public
148 * @param string $name file name
150 function setName($name)
152 $this->_name = trim($name);
156 * @access public
157 * @return string binary file content
159 function getContent($as_binary = true, $offset = 0, $length = null)
161 if (null === $this->_content) {
162 if ($this->isUploaded() && ! $this->checkUploadedFile()) {
163 return false;
166 if (! $this->isReadable()) {
167 return false;
170 if (function_exists('file_get_contents')) {
171 $this->_content = file_get_contents($this->getName());
172 } elseif ($size = filesize($this->getName())) {
173 $this->_content = fread(fopen($this->getName(), 'rb'), $size);
177 if (! empty($this->_content) && $as_binary) {
178 return '0x' . bin2hex($this->_content);
181 if (null !== $length) {
182 return substr($this->_content, $offset, $length);
183 } elseif ($offset > 0) {
184 return substr($this->_content, $offset);
187 return $this->_content;
191 * @access public
192 * @return bool
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 * @access public
212 * @param string name of file uploaded
213 * @return boolean success
215 function setUploadedFile($name)
217 $this->setName($name);
219 if (! $this->isUploaded()) {
220 $this->setName(null);
221 $this->_error_message = __('File was not an uploaded file.');
222 return false;
225 return true;
229 * @access public
230 * @param string $key the md5 hash of the column name
231 * @param string $rownumber
232 * @return boolean success
234 function setUploadedFromTblChangeRequest($key, $rownumber)
236 if (! isset($_FILES['fields_upload']) || empty($_FILES['fields_upload']['name']['multi_edit'][$rownumber][$key])) {
237 return false;
239 $file = PMA_File::fetchUploadedFromTblChangeRequestMultiple($_FILES['fields_upload'], $rownumber, $key);
241 // for blobstreaming
242 $is_bs_upload = false;
244 // check if this field requires a repository upload
245 if (isset($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key])) {
246 $is_bs_upload = ($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key] == "on") ? true : false;
248 // if request is an upload to the BLOB repository
249 if ($is_bs_upload) {
250 $bs_db = $_REQUEST['db'];
251 $bs_table = $_REQUEST['table'];
252 $tmp_filename = $file['tmp_name'];
253 $tmp_file_type = $file['type'];
255 if (! $tmp_file_type) {
256 $tmp_file_type = null;
259 if (! $bs_db || ! $bs_table) {
260 $this->_error_message = __('Unknown error while uploading.');
261 return false;
263 $blob_url = PMA_BS_UpLoadFile($bs_db, $bs_table, $tmp_file_type, $tmp_filename);
264 PMA_File::setRecentBLOBReference($blob_url);
265 } // end if ($is_bs_upload)
267 // check for file upload errors
268 switch ($file['error']) {
269 // we do not use the PHP constants here cause not all constants
270 // are defined in all versions of PHP - but the correct constants names
271 // are given as comment
272 case 0: //UPLOAD_ERR_OK:
273 return $this->setUploadedFile($file['tmp_name']);
274 break;
275 case 4: //UPLOAD_ERR_NO_FILE:
276 break;
277 case 1: //UPLOAD_ERR_INI_SIZE:
278 $this->_error_message = __('The uploaded file exceeds the upload_max_filesize directive in php.ini.');
279 break;
280 case 2: //UPLOAD_ERR_FORM_SIZE:
281 $this->_error_message = __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.');
282 break;
283 case 3: //UPLOAD_ERR_PARTIAL:
284 $this->_error_message = __('The uploaded file was only partially uploaded.');
285 break;
286 case 6: //UPLOAD_ERR_NO_TMP_DIR:
287 $this->_error_message = __('Missing a temporary folder.');
288 break;
289 case 7: //UPLOAD_ERR_CANT_WRITE:
290 $this->_error_message = __('Failed to write file to disk.');
291 break;
292 case 8: //UPLOAD_ERR_EXTENSION:
293 $this->_error_message = __('File upload stopped by extension.');
294 break;
295 default:
296 $this->_error_message = __('Unknown error in file upload.');
297 } // end switch
299 return false;
303 * strips some dimension from the multi-dimensional array from $_FILES
305 * <code>
306 * $file['name']['multi_edit'][$rownumber][$key] = [value]
307 * $file['type']['multi_edit'][$rownumber][$key] = [value]
308 * $file['size']['multi_edit'][$rownumber][$key] = [value]
309 * $file['tmp_name']['multi_edit'][$rownumber][$key] = [value]
310 * $file['error']['multi_edit'][$rownumber][$key] = [value]
312 * // becomes:
314 * $file['name'] = [value]
315 * $file['type'] = [value]
316 * $file['size'] = [value]
317 * $file['tmp_name'] = [value]
318 * $file['error'] = [value]
319 * </code>
321 * @access public
322 * @static
323 * @param array $file the array
324 * @param string $rownumber
325 * @param string $key
326 * @return array
328 function fetchUploadedFromTblChangeRequestMultiple($file, $rownumber, $key)
330 $new_file = array(
331 'name' => $file['name']['multi_edit'][$rownumber][$key],
332 'type' => $file['type']['multi_edit'][$rownumber][$key],
333 'size' => $file['size']['multi_edit'][$rownumber][$key],
334 'tmp_name' => $file['tmp_name']['multi_edit'][$rownumber][$key],
335 'error' => $file['error']['multi_edit'][$rownumber][$key],
338 return $new_file;
342 * sets the name if the file to the one selected in the tbl_change form
344 * @access public
345 * @param string $key the md5 hash of the column name
346 * @param string $rownumber
347 * @return boolean success
349 function setSelectedFromTblChangeRequest($key, $rownumber = null)
351 if (! empty($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])
352 && is_string($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key])) {
353 // ... whether with multiple rows ...
354 // for blobstreaming
355 $is_bs_upload = false;
357 // check if this field requires a repository upload
358 if (isset($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key])) {
359 $is_bs_upload = ($_REQUEST['upload_blob_repo']['multi_edit'][$rownumber][$key] == "on") ? true : false;
362 // is a request to upload file to BLOB repository using uploadDir mechanism
363 if ($is_bs_upload) {
364 $bs_db = $_REQUEST['db'];
365 $bs_table = $_REQUEST['table'];
366 $tmp_filename = $GLOBALS['cfg']['UploadDir'] . '/' . $_REQUEST['fields_uploadlocal_' . $key]['multi_edit'][$rownumber];
368 // check if fileinfo library exists
369 if ($PMA_Config->get('FILEINFO_EXISTS')) {
370 // attempt to init fileinfo
371 $finfo = finfo_open(FILEINFO_MIME);
373 // fileinfo exists
374 if ($finfo) {
375 // pass in filename to fileinfo and close fileinfo handle after
376 $tmp_file_type = finfo_file($finfo, $tmp_filename);
377 finfo_close($finfo);
379 } else {
380 // no fileinfo library exists, use file command
381 $tmp_file_type = exec("file -bi " . escapeshellarg($tmp_filename));
384 if (! $tmp_file_type) {
385 $tmp_file_type = null;
388 if (! $bs_db || !$bs_table) {
389 $this->_error_message = __('Unknown error while uploading.');
390 return false;
392 $blob_url = PMA_BS_UpLoadFile($bs_db, $bs_table, $tmp_file_type, $tmp_filename);
393 PMA_File::setRecentBLOBReference($blob_url);
394 } // end if ($is_bs_upload)
396 return $this->setLocalSelectedFile($_REQUEST['fields_uploadlocal']['multi_edit'][$rownumber][$key]);
397 } else {
398 return false;
403 * @access public
404 * @return string error message
406 function getError()
408 return $this->_error_message;
412 * @access public
413 * @return boolean whether an error occured or not
415 function isError()
417 return ! empty($this->_error_message);
421 * checks the superglobals provided if the tbl_change form is submitted
422 * and uses the submitted/selected file
424 * @access public
425 * @param string $key the md5 hash of the column name
426 * @param string $rownumber
427 * @return boolean success
429 function checkTblChangeForm($key, $rownumber)
431 if ($this->setUploadedFromTblChangeRequest($key, $rownumber)) {
432 // well done ...
433 $this->_error_message = '';
434 return true;
435 } elseif ($this->setSelectedFromTblChangeRequest($key, $rownumber)) {
436 // well done ...
437 $this->_error_message = '';
438 return true;
440 // all failed, whether just no file uploaded/selected or an error
442 return false;
447 * @access public
448 * @param string $name
449 * @return boolean success
451 function setLocalSelectedFile($name)
453 if (empty($GLOBALS['cfg']['UploadDir'])) return false;
455 $this->setName(PMA_userDir($GLOBALS['cfg']['UploadDir']) . PMA_securePath($name));
456 if (! $this->isReadable()) {
457 $this->_error_message = __('File could not be read');
458 $this->setName(null);
459 return false;
462 return true;
466 * @access public
467 * @return boolean whether the file is readable or not
469 function isReadable()
471 // suppress warnings from being displayed, but not from being logged
472 // any file access outside of open_basedir will issue a warning
473 ob_start();
474 $is_readable = is_readable($this->getName());
475 ob_end_clean();
476 return $is_readable;
480 * If we are on a server with open_basedir, we must move the file
481 * before opening it. The FAQ 1.11 explains how to create the "./tmp"
482 * directory - if needed
484 * @todo move check of $cfg['TempDir'] into PMA_Config?
485 * @access public
486 * @return boolean whether uploaded fiel is fine or not
488 function checkUploadedFile()
490 if ($this->isReadable()) {
491 return true;
494 if (empty($GLOBALS['cfg']['TempDir']) || ! is_writable($GLOBALS['cfg']['TempDir'])) {
495 // cannot create directory or access, point user to FAQ 1.11
496 $this->_error_message = __('Error moving the uploaded file, see [a@./Documentation.html#faq1_11@Documentation]FAQ 1.11[/a]');
497 return false;
500 $new_file_to_upload = tempnam(realpath($GLOBALS['cfg']['TempDir']), basename($this->getName()));
502 // suppress warnings from being displayed, but not from being logged
503 // any file access outside of open_basedir will issue a warning
504 ob_start();
505 $move_uploaded_file_result = move_uploaded_file($this->getName(), $new_file_to_upload);
506 ob_end_clean();
507 if (! $move_uploaded_file_result) {
508 $this->_error_message = __('Error while moving uploaded file.');
509 return false;
512 $this->setName($new_file_to_upload);
513 $this->isTemp(true);
515 if (! $this->isReadable()) {
516 $this->_error_message = __('Cannot read (moved) upload file.');
517 return false;
520 return true;
524 * Detects what compression filse uses
526 * @todo move file read part into readChunk() or getChunk()
527 * @todo add support for compression plugins
528 * @access protected
529 * @return string MIME type of compression, none for none
531 function _detectCompression()
533 // suppress warnings from being displayed, but not from being logged
534 // f.e. any file access outside of open_basedir will issue a warning
535 ob_start();
536 $file = fopen($this->getName(), 'rb');
537 ob_end_clean();
539 if (! $file) {
540 $this->_error_message = __('File could not be read');
541 return false;
545 * @todo
546 * get registered plugins for file compression
548 foreach (PMA_getPlugins($type = 'compression') as $plugin) {
549 if (call_user_func_array(array($plugin['classname'], 'canHandle'), array($this->getName()))) {
550 $this->setCompressionPlugin($plugin);
551 break;
556 $test = fread($file, 4);
557 $len = strlen($test);
558 fclose($file);
560 if ($len >= 2 && $test[0] == chr(31) && $test[1] == chr(139)) {
561 $this->_compression = 'application/gzip';
562 } elseif ($len >= 3 && substr($test, 0, 3) == 'BZh') {
563 $this->_compression = 'application/bzip2';
564 } elseif ($len >= 4 && $test == "PK\003\004") {
565 $this->_compression = 'application/zip';
566 } else {
567 $this->_compression = 'none';
570 return $this->_compression;
574 * whether the content should be decompressed before returned
576 function setDecompressContent($decompress)
578 $this->_decompress = (bool) $decompress;
581 function getHandle()
583 if (null === $this->_handle) {
584 $this->open();
586 return $this->_handle;
589 function setHandle($handle)
591 $this->_handle = $handle;
595 * @return bool
597 function open()
599 if (! $this->_decompress) {
600 $this->_handle = @fopen($this->getName(), 'r');
603 switch ($this->getCompression()) {
604 case false:
605 return false;
606 case 'application/bzip2':
607 if ($GLOBALS['cfg']['BZipDump'] && @function_exists('bzopen')) {
608 $this->_handle = @bzopen($this->getName(), 'r');
609 } else {
610 $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());
611 return false;
613 break;
614 case 'application/gzip':
615 if ($GLOBALS['cfg']['GZipDump'] && @function_exists('gzopen')) {
616 $this->_handle = @gzopen($this->getName(), 'r');
617 } else {
618 $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());
619 return false;
621 break;
622 case 'application/zip':
623 if ($GLOBALS['cfg']['ZipDump'] && @function_exists('zip_open')) {
624 include_once './libraries/zip_extension.lib.php';
625 $result = PMA_getZipContents($this->getName());
626 if (! empty($result['error'])) {
627 $this->_error_message = PMA_Message::rawError($result['error']);
628 return false;
629 } else {
630 $this->content_uncompressed = $result['data'];
632 unset($result);
633 } else {
634 $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());
635 return false;
637 break;
638 case 'none':
639 $this->_handle = @fopen($this->getName(), 'r');
640 break;
641 default:
642 $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());
643 return false;
644 break;
647 return true;
650 function getCharset()
652 return $this->_charset;
655 function setCharset($charset)
657 $this->_charset = $charset;
661 * @return string MIME type of compression, none for none
662 * @access public
664 function getCompression()
666 if (null === $this->_compression) {
667 return $this->_detectCompression();
670 return $this->_compression;
674 * advances the file pointer in the file handle by $length bytes/chars
676 * @param integer $length numbers of chars/bytes to skip
677 * @return boolean
678 * @todo this function is unused
680 function advanceFilePointer($length)
682 while ($length > 0) {
683 $this->getNextChunk($length);
684 $length -= $this->getChunkSize();
689 * http://bugs.php.net/bug.php?id=29532
690 * bzip reads a maximum of 8192 bytes on windows systems
691 * @todo this function is unused
692 * @param int $max_size
693 * @return bool|string
695 function getNextChunk($max_size = null)
697 if (null !== $max_size) {
698 $size = min($max_size, $this->getChunkSize());
699 } else {
700 $size = $this->getChunkSize();
703 // $result = $this->handler->getNextChunk($size);
704 $result = '';
705 switch ($this->getCompression()) {
706 case 'application/bzip2':
707 $result = '';
708 while (strlen($result) < $size - 8192 && ! feof($this->getHandle())) {
709 $result .= bzread($this->getHandle(), $size);
711 break;
712 case 'application/gzip':
713 $result = gzread($this->getHandle(), $size);
714 break;
715 case 'application/zip':
717 * if getNextChunk() is used some day,
718 * replace this code by code similar to the one
719 * in open()
721 include_once './libraries/unzip.lib.php';
722 $import_handle = new SimpleUnzip();
723 $import_handle->ReadFile($this->getName());
724 if ($import_handle->Count() == 0) {
725 $this->_error_message = __('No files found inside ZIP archive!');
726 return false;
727 } elseif ($import_handle->GetError(0) != 0) {
728 $this->_error_message = __('Error in ZIP archive:')
729 . ' ' . $import_handle->GetErrorMsg(0);
730 return false;
731 } else {
732 $result = $import_handle->GetData(0);
735 break;
736 case 'none':
737 $result = fread($this->getHandle(), $size);
738 break;
739 default:
740 return false;
743 if ($GLOBALS['charset_conversion']) {
744 $result = PMA_convert_string($this->getCharset(), 'utf-8', $result);
745 } else {
747 * Skip possible byte order marks (I do not think we need more
748 * charsets, but feel free to add more, you can use wikipedia for
749 * reference: <http://en.wikipedia.org/wiki/Byte_Order_Mark>)
751 * @todo BOM could be used for charset autodetection
753 if ($this->getOffset() === 0) {
754 // UTF-8
755 if (strncmp($result, "\xEF\xBB\xBF", 3) == 0) {
756 $result = substr($result, 3);
757 // UTF-16 BE, LE
758 } elseif (strncmp($result, "\xFE\xFF", 2) == 0
759 || strncmp($result, "\xFF\xFE", 2) == 0) {
760 $result = substr($result, 2);
765 $this->_offset += $size;
766 if (0 === $result) {
767 return true;
769 return $result;
772 function getOffset()
774 return $this->_offset;
777 function getChunkSize()
779 return $this->_chunk_size;
782 function setChunkSize($chunk_size)
784 $this->_chunk_size = (int) $chunk_size;
787 function getContentLength()
789 return strlen($this->_content);
792 function eof()
794 if ($this->getHandle()) {
795 return feof($this->getHandle());
796 } else {
797 return ($this->getOffset() >= $this->getContentLength());
803 * sets reference to most recent BLOB repository reference
805 * @access public
806 * @param string - BLOB repository reference
808 static function setRecentBLOBReference($ref)
810 PMA_File::$_recent_bs_reference = $ref;
814 * retrieves reference to most recent BLOB repository reference
816 * @access public
817 * @return string - most recent BLOB repository reference
819 static function getRecentBLOBReference()
821 $ref = PMA_File::$_recent_bs_reference;
822 PMA_File::$_recent_bs_reference = null;
824 return $ref;