Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / FileUtil.class.php
CommitLineData
281374c9
AE
1<?php
2namespace wcf\util;
ec1b1daf 3use wcf\system\exception\SystemException;
281374c9 4use wcf\system\io\File;
a628af06 5use wcf\system\io\GZipFile;
281374c9
AE
6
7/**
8 * Contains file-related functions.
9f959ced
MS
9 *
10 * @author Marcel Werk
c839bd49 11 * @copyright 2001-2018 WoltLab GmbH
281374c9 12 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 13 * @package WoltLabSuite\Core\Util
281374c9 14 */
18284789 15final class FileUtil {
35e07ccc
MW
16 /**
17 * finfo instance
9f959ced 18 * @var \finfo
35e07ccc
MW
19 */
20 protected static $finfo = null;
21
89c020cc
AE
22 /**
23 * memory limit in bytes
24 * @var integer
25 */
26 protected static $memoryLimit = null;
27
d8fa09e0
AE
28 /**
29 * chmod mode
30 * @var string
31 */
32 protected static $mode = null;
33
59e352e7
TD
34 /**
35 * Tries to find the temp folder.
36 *
37 * @return string
2b770bdd 38 * @throws SystemException
59e352e7
TD
39 */
40 public static function getTempFolder() {
40c9a81c
TD
41 try {
42 // This method does not contain any shut up operator by intent.
43 // Any operation that fails here is fatal.
44 $path = WCF_DIR.'tmp/';
45
46 if (is_file($path)) {
47 // wat
48 unlink($path);
49 }
50
51 if (!file_exists($path)) {
52 mkdir($path, 0777);
53 }
54
55 if (!is_dir($path)) {
56 throw new SystemException("Temporary folder '".$path."' does not exist and could not be created. Please check the permissions of the '".WCF_DIR."' folder using your favorite ftp program.");
57 }
58
59 if (!is_writable($path)) {
60 self::makeWritable($path);
61 }
62
63 if (!is_writable($path)) {
64 throw new SystemException("Temporary folder '".$path."' is not writable. Please check the permissions using your favorite ftp program.");
65 }
66
67 file_put_contents($path.'/.htaccess', 'deny from all');
68
69 return $path;
59e352e7 70 }
40c9a81c
TD
71 catch (SystemException $e) {
72 // use tmp folder in document root by default
73 if (!empty($_SERVER['DOCUMENT_ROOT'])) {
74 if (strpos($_SERVER['DOCUMENT_ROOT'], 'strato') !== false) {
75 // strato bugfix
76 // create tmp folder in document root automatically
77 if (!@file_exists($_SERVER['DOCUMENT_ROOT'].'/tmp')) {
78 @mkdir($_SERVER['DOCUMENT_ROOT'].'/tmp/', 0777);
79 self::makeWritable($_SERVER['DOCUMENT_ROOT'].'/tmp/');
80 }
81 }
82 if (@file_exists($_SERVER['DOCUMENT_ROOT'].'/tmp') && @is_writable($_SERVER['DOCUMENT_ROOT'].'/tmp')) {
83 return $_SERVER['DOCUMENT_ROOT'].'/tmp/';
84 }
85 }
86
87 if (isset($_ENV['TMP']) && @is_writable($_ENV['TMP'])) {
88 return $_ENV['TMP'] . '/';
89 }
90 if (isset($_ENV['TEMP']) && @is_writable($_ENV['TEMP'])) {
91 return $_ENV['TEMP'] . '/';
92 }
93 if (isset($_ENV['TMPDIR']) && @is_writable($_ENV['TMPDIR'])) {
94 return $_ENV['TMPDIR'] . '/';
95 }
96
97 if (($path = ini_get('upload_tmp_dir')) && @is_writable($path)) {
98 return $path . '/';
99 }
100 if (@file_exists('/tmp/') && @is_writable('/tmp/')) {
101 return '/tmp/';
102 }
103 if (function_exists('session_save_path') && ($path = session_save_path()) && @is_writable($path)) {
104 return $path . '/';
105 }
106
107 throw new SystemException('There is no access to the system temporary folder due to an unknown reason and no user specific temporary folder exists in '.WCF_DIR.'! This is a misconfiguration of your webserver software! Please create a folder called '.$path.' using your favorite ftp program, make it writable and then retry this installation.');
59e352e7
TD
108 }
109 }
110
281374c9
AE
111 /**
112 * Generates a new temporary filename in TMP_DIR.
9f959ced
MS
113 *
114 * @param string $prefix
115 * @param string $extension
116 * @param string $dir
117 * @return string
281374c9
AE
118 */
119 public static function getTemporaryFilename($prefix = 'tmpFile_', $extension = '', $dir = TMP_DIR) {
120 $dir = self::addTrailingSlash($dir);
121 do {
122 $tmpFile = $dir.$prefix.StringUtil::getRandomID().$extension;
123 }
59e352e7 124 while (file_exists($tmpFile));
281374c9
AE
125
126 return $tmpFile;
127 }
128
129 /**
9f959ced 130 * Removes a leading slash from the given path.
59dc0db6 131 *
9f959ced
MS
132 * @param string $path
133 * @return string
281374c9
AE
134 */
135 public static function removeLeadingSlash($path) {
4c53e034 136 return ltrim($path, '/');
281374c9 137 }
9f959ced 138
281374c9 139 /**
9f959ced
MS
140 * Removes a trailing slash from the given path.
141 *
142 * @param string $path
143 * @return string
281374c9
AE
144 */
145 public static function removeTrailingSlash($path) {
4c53e034 146 return rtrim($path, '/');
281374c9
AE
147 }
148
149 /**
9f959ced
MS
150 * Adds a trailing slash to the given path.
151 *
152 * @param string $path
153 * @return string
281374c9
AE
154 */
155 public static function addTrailingSlash($path) {
4c53e034 156 return rtrim($path, '/').'/';
c244fcc6
AE
157 }
158
159 /**
9f959ced 160 * Adds a leading slash to the given path.
c244fcc6
AE
161 *
162 * @param string $path
9f959ced 163 * @return string
c244fcc6
AE
164 */
165 public static function addLeadingSlash($path) {
4c53e034 166 return '/'.ltrim($path, '/');
281374c9 167 }
9f959ced 168
281374c9 169 /**
9f959ced
MS
170 * Returns the relative path from the given absolute paths.
171 *
172 * @param string $currentDir
173 * @param string $targetDir
174 * @return string
281374c9
AE
175 */
176 public static function getRelativePath($currentDir, $targetDir) {
177 // remove trailing slashes
f2024036
AE
178 $currentDir = self::removeTrailingSlash(self::unifyDirSeparator($currentDir));
179 $targetDir = self::removeTrailingSlash(self::unifyDirSeparator($targetDir));
281374c9
AE
180
181 if ($currentDir == $targetDir) {
59e352e7 182 return './';
281374c9
AE
183 }
184
185 $current = explode('/', $currentDir);
186 $target = explode('/', $targetDir);
187
188 $relPath = '';
189 //for ($i = max(count($current), count($target)) - 1; $i >= 0; $i--) {
190 for ($i = 0, $max = max(count($current), count($target)); $i < $max; $i++) {
191 if (isset($current[$i]) && isset($target[$i])) {
192 if ($current[$i] != $target[$i]) {
193 for ($j = 0; $j < $i; $j++) {
59e352e7 194 unset($target[$j]);
281374c9 195 }
f4683ba3 196 $relPath .= str_repeat('../', count($current) - $i).implode('/', $target).'/';
29ddf8ad 197
281374c9
AE
198 break;
199 }
97e52431 200 }
281374c9
AE
201 // go up one level
202 else if (isset($current[$i]) && !isset($target[$i])) {
203 $relPath .= '../';
204 }
205 else if (!isset($current[$i]) && isset($target[$i])) {
206 $relPath .= $target[$i].'/';
207 }
208 }
209
210 return $relPath;
211 }
212
213 /**
9f959ced
MS
214 * Creates a path on the local filesystem and returns true on success.
215 * Parent directories do not need to exists as they will be created if
216 * necessary.
281374c9 217 *
9f959ced 218 * @param string $path
9f959ced 219 * @return boolean
281374c9 220 */
1232bce2 221 public static function makePath($path) {
281374c9
AE
222 // directory already exists, abort
223 if (file_exists($path)) {
224 return false;
225 }
226
227 // check if parent directory exists
228 $parent = dirname($path);
229 if ($parent != $path) {
230 // parent directory does not exist either
231 // we have to create the parent directory first
232 $parent = self::addTrailingSlash($parent);
233 if (!@file_exists($parent)) {
234 // could not create parent directory either => abort
1232bce2 235 if (!self::makePath($parent)) {
59e352e7 236 return false;
281374c9
AE
237 }
238 }
239
240 // well, the parent directory exists or has been created
241 // lets create this path
1232bce2 242 if (!@mkdir($path)) {
281374c9
AE
243 return false;
244 }
1232bce2
AE
245
246 self::makeWritable($path);
281374c9
AE
247
248 return true;
249 }
250
251 return false;
252 }
253
254 /**
f2024036 255 * Unifies windows and unix directory separators.
9f959ced
MS
256 *
257 * @param string $path
258 * @return string
281374c9 259 */
f2024036 260 public static function unifyDirSeparator($path) {
281374c9
AE
261 $path = str_replace('\\\\', '/', $path);
262 $path = str_replace('\\', '/', $path);
263 return $path;
264 }
265
266 /**
267 * Scans a folder (and subfolder) for a specific file.
268 * Returns the filename if found, otherwise false.
9f959ced
MS
269 *
270 * @param string $folder
271 * @param string $searchfile
272 * @param boolean $recursive
273 * @return mixed
281374c9
AE
274 */
275 public static function scanFolder($folder, $searchfile, $recursive = true) {
276 if (!@is_dir($folder)) {
277 return false;
278 }
279 if (!$searchfile) {
280 return false;
281 }
9f959ced 282
281374c9
AE
283 $folder = self::addTrailingSlash($folder);
284 $dirh = @opendir($folder);
285 while ($filename = @readdir($dirh)) {
286 if ($filename == '.' || $filename == '..') {
287 continue;
288 }
289 if ($filename == $searchfile) {
290 @closedir($dirh);
291 return $folder.$filename;
292 }
9f959ced 293
281374c9
AE
294 if ($recursive == true && @is_dir($folder.$filename)) {
295 if ($found = self::scanFolder($folder.$filename, $searchfile, $recursive)) {
296 @closedir($dirh);
297 return $found;
298 }
299 }
300 }
301 @closedir($dirh);
302 }
303
304 /**
28410a97 305 * Returns true if the given filename is an url (http or ftp).
281374c9 306 *
9f959ced 307 * @param string $filename
281374c9
AE
308 * @return boolean
309 */
310 public static function isURL($filename) {
311 return preg_match('!^(https?|ftp)://!', $filename);
312 }
313
314 /**
315 * Returns canonicalized absolute pathname.
316 *
317 * @param string $path
318 * @return string path
319 */
320 public static function getRealPath($path) {
f2024036 321 $path = self::unifyDirSeparator($path);
281374c9 322
59ab4d0f 323 $result = [];
281374c9
AE
324 $pathA = explode('/', $path);
325 if ($pathA[0] === '') {
326 $result[] = '';
327 }
9f959ced 328
281374c9
AE
329 foreach ($pathA as $key => $dir) {
330 if ($dir == '..') {
331 if (end($result) == '..') {
332 $result[] = '..';
0f0590c2
MS
333 }
334 else {
281374c9
AE
335 $lastValue = array_pop($result);
336 if ($lastValue === '' || $lastValue === null) {
337 $result[] = '..';
338 }
339 }
0f0590c2 340 }
281374c9
AE
341 else if ($dir !== '' && $dir != '.') {
342 $result[] = $dir;
343 }
344 }
345
346 $lastValue = end($pathA);
347 if ($lastValue === '' || $lastValue === false) {
348 $result[] = '';
349 }
350
351 return implode('/', $result);
352 }
353
354 /**
9f959ced
MS
355 * Formats the given filesize.
356 *
357 * @param integer $byte
358 * @param integer $precision
359 * @return string
281374c9
AE
360 */
361 public static function formatFilesize($byte, $precision = 2) {
362 $symbol = 'Byte';
363 if ($byte >= 1000) {
364 $byte /= 1000;
365 $symbol = 'kB';
366 }
367 if ($byte >= 1000) {
368 $byte /= 1000;
369 $symbol = 'MB';
370 }
371 if ($byte >= 1000) {
372 $byte /= 1000;
373 $symbol = 'GB';
374 }
375 if ($byte >= 1000) {
376 $byte /= 1000;
377 $symbol = 'TB';
378 }
379
380 return StringUtil::formatNumeric(round($byte, $precision)).' '.$symbol;
381 }
382
383 /**
9f959ced 384 * Formats a filesize with binary prefix.
281374c9 385 *
1615fc2e 386 * For more information: <http://en.wikipedia.org/wiki/Binary_prefix>
9f959ced
MS
387 *
388 * @param integer $byte
389 * @param integer $precision
390 * @return string
281374c9
AE
391 */
392 public static function formatFilesizeBinary($byte, $precision = 2) {
393 $symbol = 'Byte';
394 if ($byte >= 1024) {
395 $byte /= 1024;
396 $symbol = 'KiB';
397 }
398 if ($byte >= 1024) {
399 $byte /= 1024;
400 $symbol = 'MiB';
401 }
402 if ($byte >= 1024) {
403 $byte /= 1024;
404 $symbol = 'GiB';
405 }
406 if ($byte >= 1024) {
407 $byte /= 1024;
408 $symbol = 'TiB';
409 }
410
411 return StringUtil::formatNumeric(round($byte, $precision)).' '.$symbol;
412 }
413
414 /**
9f959ced
MS
415 * Downloads a package archive from an http URL and returns the path to
416 * the downloaded file.
281374c9
AE
417 *
418 * @param string $httpUrl
419 * @param string $prefix
a0ac592e
TD
420 * @param array $options
421 * @param array $postParameters
9f959ced
MS
422 * @param array $headers empty array or a not initialized variable
423 * @return string
e3369fd2 424 * @deprecated This method currently only is a wrapper around \wcf\util\HTTPRequest. Please use
f9d24625 425 * HTTPRequest from now on, as this method may be removed in the future.
281374c9 426 */
59ab4d0f 427 public static function downloadFileFromHttp($httpUrl, $prefix = 'package', array $options = [], array $postParameters = [], &$headers = []) {
a195ffa6 428 $request = new HTTPRequest($httpUrl, $options, $postParameters);
86fc0430
TD
429 $request->execute();
430 $reply = $request->getReply();
09727da6 431
86fc0430
TD
432 $newFileName = self::getTemporaryFilename($prefix.'_');
433 file_put_contents($newFileName, $reply['body']); // the file to write.
09727da6 434
86fc0430
TD
435 $tmp = $reply['headers']; // copy variable, to avoid problems with the reference
436 $headers = $tmp;
281374c9 437
59e352e7 438 return $newFileName;
281374c9
AE
439 }
440
281374c9
AE
441 /**
442 * Determines whether a file is text or binary by checking the first few bytes in the file.
443 * The exact number of bytes is system dependent, but it is typically several thousand.
444 * If every byte in that part of the file is non-null, considers the file to be text;
445 * otherwise it considers the file to be binary.
446 *
447 * @param string $file
9f959ced 448 * @return boolean
281374c9
AE
449 */
450 public static function isBinary($file) {
451 // open file
452 $file = new File($file, 'rb');
453
454 // get block size
455 $stat = $file->stat();
456 $blockSize = $stat['blksize'];
457 if ($blockSize < 0) $blockSize = 1024;
458 if ($blockSize > $file->filesize()) $blockSize = $file->filesize();
459 if ($blockSize <= 0) return false;
460
461 // get bytes
462 $block = $file->read($blockSize);
56624595 463 return (strlen($block) == 0 || strpos($block, "\0") !== false);
281374c9
AE
464 }
465
466 /**
9f959ced
MS
467 * Uncompresses a gzipped file and returns true if successful.
468 *
469 * @param string $gzipped
470 * @param string $destination
e3369fd2 471 * @return boolean
281374c9
AE
472 */
473 public static function uncompressFile($gzipped, $destination) {
474 if (!@is_file($gzipped)) {
59e352e7 475 return false;
281374c9
AE
476 }
477
a628af06 478 $sourceFile = new GZipFile($gzipped, 'rb');
281374c9
AE
479 //$filesize = $sourceFile->getFileSize();
480 $targetFile = new File($destination);
481 while (!$sourceFile->eof()) {
59e352e7 482 $targetFile->write($sourceFile->read(512), 512);
281374c9
AE
483 }
484 $targetFile->close();
485 $sourceFile->close();
281374c9 486
1232bce2 487 self::makeWritable($destination);
281374c9 488
59e352e7 489 return true;
281374c9
AE
490 }
491
492 /**
28410a97 493 * Returns true if php is running as apache module.
281374c9 494 *
9f959ced 495 * @return boolean
281374c9
AE
496 */
497 public static function isApacheModule() {
498 return function_exists('apache_get_version');
499 }
35e07ccc
MW
500
501 /**
502 * Returns the mime type of a file.
503 *
504 * @param string $filename
505 * @return string
506 */
507 public static function getMimeType($filename) {
508 if (self::$finfo === null) {
0e8c6d5e 509 if (!class_exists('\finfo', false)) return 'application/octet-stream';
35e07ccc
MW
510 self::$finfo = new \finfo(FILEINFO_MIME_TYPE);
511 }
512
07673afa
AE
513 // \finfo->file() can fail for files that contain only 1 byte, because libmagic expects at least
514 // a few bytes in order to determine the type. See https://bugs.php.net/bug.php?id=64684
515 $mimeType = @self::$finfo->file($filename);
516 return $mimeType ?: 'application/octet-stream';
35e07ccc 517 }
18284789 518
1232bce2
AE
519 /**
520 * Tries to make a file or directory writable. It starts of with the least
d8fa09e0 521 * permissions and goes up until 0666 for files and 0777 for directories.
1232bce2
AE
522 *
523 * @param string $filename
2b770bdd 524 * @throws SystemException
1232bce2
AE
525 */
526 public static function makeWritable($filename) {
043b049d 527 if (!file_exists($filename)) {
1232bce2
AE
528 return;
529 }
530
d8fa09e0 531 if (self::$mode === null) {
850e5402 532 // WCFSetup
97e52431 533 if (defined('INSTALL_SCRIPT') && file_exists(INSTALL_SCRIPT)) {
850e5402 534 // do not use PHP_OS here, as this represents the system it was built on != running on
0436b618
AE
535 // php_uname() is forbidden on some strange hosts; PHP_EOL is reliable
536 if (PHP_EOL == "\r\n") {
537 // Windows
53e6fba3 538 self::$mode = '0777';
850e5402
AE
539 }
540 else {
0436b618 541 // anything but Windows
adbd8054
AE
542 clearstatcache();
543
884dc05a 544 self::$mode = '0666';
7fe5312d 545
850e5402
AE
546 $tmpFilename = '__permissions_'.sha1(time()).'.txt';
547 @touch($tmpFilename);
7fe5312d 548
850e5402
AE
549 // create a new file and check the file owner, if it is the same
550 // as this file (uploaded through FTP), we can safely grant write
551 // permissions exclusively to the owner rather than everyone
552 if (file_exists($tmpFilename)) {
728b9dd6 553 $scriptOwner = fileowner(INSTALL_SCRIPT);
850e5402 554 $fileOwner = fileowner($tmpFilename);
7fe5312d 555
850e5402 556 if ($scriptOwner === $fileOwner) {
884dc05a 557 self::$mode = '0644';
850e5402 558 }
7fe5312d 559
850e5402
AE
560 @unlink($tmpFilename);
561 }
562 }
563 }
564 else {
565 // mirror permissions of WCF.class.php
49fd1c14 566 if (!file_exists(WCF_DIR . 'lib/system/WCF.class.php')) {
850e5402
AE
567 throw new SystemException("Unable to find 'wcf/lib/system/WCF.class.php'.");
568 }
569
570 self::$mode = '0' . substr(sprintf('%o', fileperms(WCF_DIR . 'lib/system/WCF.class.php')), -3);
d8fa09e0 571 }
1232bce2
AE
572 }
573
d8fa09e0
AE
574 if (is_dir($filename)) {
575 if (self::$mode == '0644') {
7fe5312d 576 @chmod($filename, 0755);
1232bce2 577 }
d8fa09e0 578 else {
7fe5312d 579 @chmod($filename, 0777);
1232bce2
AE
580 }
581 }
d8fa09e0 582 else {
7fe5312d 583 @chmod($filename, octdec(self::$mode));
d8fa09e0
AE
584 }
585
586 if (!is_writable($filename)) {
587 // does not work with 0777
588 throw new SystemException("Unable to make '".$filename."' writable. This is a misconfiguration of your server, please contact your system administrator or hosting provider.");
589 }
1232bce2
AE
590 }
591
89c020cc
AE
592 /**
593 * Returns memory limit in bytes.
594 *
595 * @return integer
596 */
597 public static function getMemoryLimit() {
598 if (self::$memoryLimit === null) {
599 self::$memoryLimit = 0;
600
601 $memoryLimit = ini_get('memory_limit');
602
603 // no limit
604 if ($memoryLimit == -1) {
605 self::$memoryLimit = -1;
606 }
607
608 // completely numeric, PHP assumes byte
609 if (is_numeric($memoryLimit)) {
610 self::$memoryLimit = $memoryLimit;
611 }
612
613 // PHP supports 'K', 'M' and 'G' shorthand notation
f000565d
MW
614 if (preg_match('~^(\d+)\s*([KMG])$~i', $memoryLimit, $matches)) {
615 switch (strtoupper($matches[2])) {
89c020cc
AE
616 case 'K':
617 self::$memoryLimit = $matches[1] * 1024;
618 break;
619
620 case 'M':
621 self::$memoryLimit = $matches[1] * 1024 * 1024;
622 break;
623
624 case 'G':
625 self::$memoryLimit = $matches[1] * 1024 * 1024 * 1024;
626 break;
627 }
628 }
629 }
630
631 return self::$memoryLimit;
632 }
633
4a99533b
MS
634 /**
635 * Returns true if the given amount of memory is available.
636 *
637 * @param integer $neededMemory
638 * @return boolean
639 */
640 public static function checkMemoryLimit($neededMemory) {
641 return self::getMemoryLimit() == -1 || self::getMemoryLimit() > (memory_get_usage() + $neededMemory);
642 }
643
5552d2cc
MW
644 /**
645 * Returns icon name for given filename.
646 *
647 * @param string $filename
648 * @return string
649 */
650 public static function getIconNameByFilename($filename) {
651 static $mapping = [
652 // archive
653 'zip' => 'archive', 'rar' => 'archive', 'tar' => 'archive', 'gz' => 'archive',
654 // audio
655 'mp3' => 'audio', 'ogg' => 'audio', 'wav' => 'audio',
656 // code
657 'php' => 'code', 'html' => 'code', 'htm' => 'code', 'tpl' => 'code', 'js' => 'code',
658 // excel
659 'xls' => 'excel', 'ods' => 'excel', 'xlsx' => 'excel',
660 // image
661 'gif' => 'image', 'jpg' => 'image', 'jpeg' => 'image', 'png' => 'image', 'bmp' => 'image',
662 // video
663 'avi' => 'video', 'wmv' => 'video', 'mov' => 'video', 'mp4' => 'video', 'mpg' => 'video', 'mpeg' => 'video', 'flv' => 'video',
664 // pdf
665 'pdf' => 'pdf',
666 // powerpoint
667 'ppt' => 'powerpoint', 'pptx' => 'powerpoint',
668 // text
669 'txt' => 'text',
670 // word
671 'doc' => 'word', 'docx' => 'word', 'odt' => 'word'
672 ];
673
674 $lastDotPosition = strrpos($filename, '.');
675 if ($lastDotPosition !== false) {
676 $extension = substr($filename, $lastDotPosition + 1);
677 if (isset($mapping[$extension])) {
678 return $mapping[$extension];
679 }
680 }
681
682 return '';
683 }
684
1d5f9363
MS
685 /**
686 * Forbid creation of FileUtil objects.
687 */
688 private function __construct() {
689 // does nothing
690 }
dcb3a44c 691}