1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Ardinis <Ardinis@users.noreply.github.com> 6 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 7 * @author Bart Visscher <bartv@thisnet.nl> 8 * @author Björn Schießle <bjoern@schiessle.org> 9 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 10 * @author Daniel Kesselberg <mail@danielkesselberg.de> 11 * @author Felix Moeller <mail@felixmoeller.de> 12 * @author J0WI <J0WI@users.noreply.github.com> 13 * @author Jakob Sack <mail@jakobsack.de> 14 * @author Jan-Christoph Borchardt <hey@jancborchardt.net> 15 * @author Joas Schilling <coding@schilljs.com> 16 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 17 * @author Julius Härtl <jus@bitgrid.net> 18 * @author Lukas Reschke <lukas@statuscode.ch> 19 * @author Morris Jobke <hey@morrisjobke.de> 20 * @author Olivier Paroz <github@oparoz.com> 21 * @author Pellaeon Lin <nfsmwlin@gmail.com> 22 * @author RealRancor <fisch.666@gmx.de> 23 * @author Robin Appelman <robin@icewind.nl> 24 * @author Robin McCorkell <robin@mccorkell.me.uk> 25 * @author Roeland Jago Douma <roeland@famdouma.nl> 26 * @author Simon Könnecke <simonkoennecke@gmail.com> 27 * @author Thomas Müller <thomas.mueller@tmit.eu> 28 * @author Thomas Tanghus <thomas@tanghus.net> 29 * @author Vincent Petry <vincent@nextcloud.com> 30 * 31 * @license AGPL-3.0 32 * 33 * This code is free software: you can redistribute it and/or modify 34 * it under the terms of the GNU Affero General Public License, version 3, 35 * as published by the Free Software Foundation. 36 * 37 * This program is distributed in the hope that it will be useful, 38 * but WITHOUT ANY WARRANTY; without even the implied warranty of 39 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 40 * GNU Affero General Public License for more details. 41 * 42 * You should have received a copy of the GNU Affero General Public License, version 3, 43 * along with this program. If not, see <http://www.gnu.org/licenses/> 44 * 45 */ 46use bantu\IniGetWrapper\IniGetWrapper; 47use OCP\Files\Mount\IMountPoint; 48use OCP\IUser; 49use Symfony\Component\Process\ExecutableFinder; 50 51/** 52 * Collection of useful functions 53 */ 54class OC_Helper { 55 private static $templateManager; 56 57 /** 58 * Make a human file size 59 * @param int $bytes file size in bytes 60 * @return string a human readable file size 61 * 62 * Makes 2048 to 2 kB. 63 */ 64 public static function humanFileSize($bytes) { 65 if ($bytes < 0) { 66 return "?"; 67 } 68 if ($bytes < 1024) { 69 return "$bytes B"; 70 } 71 $bytes = round($bytes / 1024, 0); 72 if ($bytes < 1024) { 73 return "$bytes KB"; 74 } 75 $bytes = round($bytes / 1024, 1); 76 if ($bytes < 1024) { 77 return "$bytes MB"; 78 } 79 $bytes = round($bytes / 1024, 1); 80 if ($bytes < 1024) { 81 return "$bytes GB"; 82 } 83 $bytes = round($bytes / 1024, 1); 84 if ($bytes < 1024) { 85 return "$bytes TB"; 86 } 87 88 $bytes = round($bytes / 1024, 1); 89 return "$bytes PB"; 90 } 91 92 /** 93 * Make a computer file size 94 * @param string $str file size in human readable format 95 * @return float|bool a file size in bytes 96 * 97 * Makes 2kB to 2048. 98 * 99 * Inspired by: https://www.php.net/manual/en/function.filesize.php#92418 100 */ 101 public static function computerFileSize($str) { 102 $str = strtolower($str); 103 if (is_numeric($str)) { 104 return (float)$str; 105 } 106 107 $bytes_array = [ 108 'b' => 1, 109 'k' => 1024, 110 'kb' => 1024, 111 'mb' => 1024 * 1024, 112 'm' => 1024 * 1024, 113 'gb' => 1024 * 1024 * 1024, 114 'g' => 1024 * 1024 * 1024, 115 'tb' => 1024 * 1024 * 1024 * 1024, 116 't' => 1024 * 1024 * 1024 * 1024, 117 'pb' => 1024 * 1024 * 1024 * 1024 * 1024, 118 'p' => 1024 * 1024 * 1024 * 1024 * 1024, 119 ]; 120 121 $bytes = (float)$str; 122 123 if (preg_match('#([kmgtp]?b?)$#si', $str, $matches) && !empty($bytes_array[$matches[1]])) { 124 $bytes *= $bytes_array[$matches[1]]; 125 } else { 126 return false; 127 } 128 129 $bytes = round($bytes); 130 131 return $bytes; 132 } 133 134 /** 135 * Recursive copying of folders 136 * @param string $src source folder 137 * @param string $dest target folder 138 * 139 */ 140 public static function copyr($src, $dest) { 141 if (is_dir($src)) { 142 if (!is_dir($dest)) { 143 mkdir($dest); 144 } 145 $files = scandir($src); 146 foreach ($files as $file) { 147 if ($file != "." && $file != "..") { 148 self::copyr("$src/$file", "$dest/$file"); 149 } 150 } 151 } elseif (file_exists($src) && !\OC\Files\Filesystem::isFileBlacklisted($src)) { 152 copy($src, $dest); 153 } 154 } 155 156 /** 157 * Recursive deletion of folders 158 * @param string $dir path to the folder 159 * @param bool $deleteSelf if set to false only the content of the folder will be deleted 160 * @return bool 161 */ 162 public static function rmdirr($dir, $deleteSelf = true) { 163 if (is_dir($dir)) { 164 $files = new RecursiveIteratorIterator( 165 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 166 RecursiveIteratorIterator::CHILD_FIRST 167 ); 168 169 foreach ($files as $fileInfo) { 170 /** @var SplFileInfo $fileInfo */ 171 if ($fileInfo->isLink()) { 172 unlink($fileInfo->getPathname()); 173 } elseif ($fileInfo->isDir()) { 174 rmdir($fileInfo->getRealPath()); 175 } else { 176 unlink($fileInfo->getRealPath()); 177 } 178 } 179 if ($deleteSelf) { 180 rmdir($dir); 181 } 182 } elseif (file_exists($dir)) { 183 if ($deleteSelf) { 184 unlink($dir); 185 } 186 } 187 if (!$deleteSelf) { 188 return true; 189 } 190 191 return !file_exists($dir); 192 } 193 194 /** 195 * @deprecated 18.0.0 196 * @return \OC\Files\Type\TemplateManager 197 */ 198 public static function getFileTemplateManager() { 199 if (!self::$templateManager) { 200 self::$templateManager = new \OC\Files\Type\TemplateManager(); 201 } 202 return self::$templateManager; 203 } 204 205 /** 206 * detect if a given program is found in the search PATH 207 * 208 * @param string $name 209 * @param bool $path 210 * @internal param string $program name 211 * @internal param string $optional search path, defaults to $PATH 212 * @return bool true if executable program found in path 213 */ 214 public static function canExecute($name, $path = false) { 215 // path defaults to PATH from environment if not set 216 if ($path === false) { 217 $path = getenv("PATH"); 218 } 219 // we look for an executable file of that name 220 $exts = [""]; 221 $check_fn = "is_executable"; 222 // Default check will be done with $path directories : 223 $dirs = explode(PATH_SEPARATOR, $path); 224 // WARNING : We have to check if open_basedir is enabled : 225 $obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir'); 226 if ($obd != "none") { 227 $obd_values = explode(PATH_SEPARATOR, $obd); 228 if (count($obd_values) > 0 and $obd_values[0]) { 229 // open_basedir is in effect ! 230 // We need to check if the program is in one of these dirs : 231 $dirs = $obd_values; 232 } 233 } 234 foreach ($dirs as $dir) { 235 foreach ($exts as $ext) { 236 if ($check_fn("$dir/$name" . $ext)) { 237 return true; 238 } 239 } 240 } 241 return false; 242 } 243 244 /** 245 * copy the contents of one stream to another 246 * 247 * @param resource $source 248 * @param resource $target 249 * @return array the number of bytes copied and result 250 */ 251 public static function streamCopy($source, $target) { 252 if (!$source or !$target) { 253 return [0, false]; 254 } 255 $bufSize = 8192; 256 $result = true; 257 $count = 0; 258 while (!feof($source)) { 259 $buf = fread($source, $bufSize); 260 $bytesWritten = fwrite($target, $buf); 261 if ($bytesWritten !== false) { 262 $count += $bytesWritten; 263 } 264 // note: strlen is expensive so only use it when necessary, 265 // on the last block 266 if ($bytesWritten === false 267 || ($bytesWritten < $bufSize && $bytesWritten < strlen($buf)) 268 ) { 269 // write error, could be disk full ? 270 $result = false; 271 break; 272 } 273 } 274 return [$count, $result]; 275 } 276 277 /** 278 * Adds a suffix to the name in case the file exists 279 * 280 * @param string $path 281 * @param string $filename 282 * @return string 283 */ 284 public static function buildNotExistingFileName($path, $filename) { 285 $view = \OC\Files\Filesystem::getView(); 286 return self::buildNotExistingFileNameForView($path, $filename, $view); 287 } 288 289 /** 290 * Adds a suffix to the name in case the file exists 291 * 292 * @param string $path 293 * @param string $filename 294 * @return string 295 */ 296 public static function buildNotExistingFileNameForView($path, $filename, \OC\Files\View $view) { 297 if ($path === '/') { 298 $path = ''; 299 } 300 if ($pos = strrpos($filename, '.')) { 301 $name = substr($filename, 0, $pos); 302 $ext = substr($filename, $pos); 303 } else { 304 $name = $filename; 305 $ext = ''; 306 } 307 308 $newpath = $path . '/' . $filename; 309 if ($view->file_exists($newpath)) { 310 if (preg_match_all('/\((\d+)\)/', $name, $matches, PREG_OFFSET_CAPTURE)) { 311 //Replace the last "(number)" with "(number+1)" 312 $last_match = count($matches[0]) - 1; 313 $counter = $matches[1][$last_match][0] + 1; 314 $offset = $matches[0][$last_match][1]; 315 $match_length = strlen($matches[0][$last_match][0]); 316 } else { 317 $counter = 2; 318 $match_length = 0; 319 $offset = false; 320 } 321 do { 322 if ($offset) { 323 //Replace the last "(number)" with "(number+1)" 324 $newname = substr_replace($name, '(' . $counter . ')', $offset, $match_length); 325 } else { 326 $newname = $name . ' (' . $counter . ')'; 327 } 328 $newpath = $path . '/' . $newname . $ext; 329 $counter++; 330 } while ($view->file_exists($newpath)); 331 } 332 333 return $newpath; 334 } 335 336 /** 337 * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is. 338 * 339 * @param array $input The array to work on 340 * @param int $case Either MB_CASE_UPPER or MB_CASE_LOWER (default) 341 * @param string $encoding The encoding parameter is the character encoding. Defaults to UTF-8 342 * @return array 343 * 344 * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is. 345 * based on https://www.php.net/manual/en/function.array-change-key-case.php#107715 346 * 347 */ 348 public static function mb_array_change_key_case($input, $case = MB_CASE_LOWER, $encoding = 'UTF-8') { 349 $case = ($case != MB_CASE_UPPER) ? MB_CASE_LOWER : MB_CASE_UPPER; 350 $ret = []; 351 foreach ($input as $k => $v) { 352 $ret[mb_convert_case($k, $case, $encoding)] = $v; 353 } 354 return $ret; 355 } 356 357 /** 358 * performs a search in a nested array 359 * @param array $haystack the array to be searched 360 * @param string $needle the search string 361 * @param mixed $index optional, only search this key name 362 * @return mixed the key of the matching field, otherwise false 363 * 364 * performs a search in a nested array 365 * 366 * taken from https://www.php.net/manual/en/function.array-search.php#97645 367 */ 368 public static function recursiveArraySearch($haystack, $needle, $index = null) { 369 $aIt = new RecursiveArrayIterator($haystack); 370 $it = new RecursiveIteratorIterator($aIt); 371 372 while ($it->valid()) { 373 if (((isset($index) and ($it->key() == $index)) or !isset($index)) and ($it->current() == $needle)) { 374 return $aIt->key(); 375 } 376 377 $it->next(); 378 } 379 380 return false; 381 } 382 383 /** 384 * calculates the maximum upload size respecting system settings, free space and user quota 385 * 386 * @param string $dir the current folder where the user currently operates 387 * @param int $freeSpace the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly 388 * @return int number of bytes representing 389 */ 390 public static function maxUploadFilesize($dir, $freeSpace = null) { 391 if (is_null($freeSpace) || $freeSpace < 0) { 392 $freeSpace = self::freeSpace($dir); 393 } 394 return min($freeSpace, self::uploadLimit()); 395 } 396 397 /** 398 * Calculate free space left within user quota 399 * 400 * @param string $dir the current folder where the user currently operates 401 * @return int number of bytes representing 402 */ 403 public static function freeSpace($dir) { 404 $freeSpace = \OC\Files\Filesystem::free_space($dir); 405 if ($freeSpace < \OCP\Files\FileInfo::SPACE_UNLIMITED) { 406 $freeSpace = max($freeSpace, 0); 407 return $freeSpace; 408 } else { 409 return (INF > 0)? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 410 } 411 } 412 413 /** 414 * Calculate PHP upload limit 415 * 416 * @return int PHP upload file size limit 417 */ 418 public static function uploadLimit() { 419 $ini = \OC::$server->get(IniGetWrapper::class); 420 $upload_max_filesize = OCP\Util::computerFileSize($ini->get('upload_max_filesize')); 421 $post_max_size = OCP\Util::computerFileSize($ini->get('post_max_size')); 422 if ((int)$upload_max_filesize === 0 and (int)$post_max_size === 0) { 423 return INF; 424 } elseif ((int)$upload_max_filesize === 0 or (int)$post_max_size === 0) { 425 return max($upload_max_filesize, $post_max_size); //only the non 0 value counts 426 } else { 427 return min($upload_max_filesize, $post_max_size); 428 } 429 } 430 431 /** 432 * Checks if a function is available 433 * 434 * @param string $function_name 435 * @return bool 436 */ 437 public static function is_function_enabled($function_name) { 438 if (!function_exists($function_name)) { 439 return false; 440 } 441 $ini = \OC::$server->get(IniGetWrapper::class); 442 $disabled = explode(',', $ini->get('disable_functions') ?: ''); 443 $disabled = array_map('trim', $disabled); 444 if (in_array($function_name, $disabled)) { 445 return false; 446 } 447 $disabled = explode(',', $ini->get('suhosin.executor.func.blacklist') ?: ''); 448 $disabled = array_map('trim', $disabled); 449 if (in_array($function_name, $disabled)) { 450 return false; 451 } 452 return true; 453 } 454 455 /** 456 * Try to find a program 457 * 458 * @param string $program 459 * @return null|string 460 */ 461 public static function findBinaryPath($program) { 462 $memcache = \OC::$server->getMemCacheFactory()->createDistributed('findBinaryPath'); 463 if ($memcache->hasKey($program)) { 464 return $memcache->get($program); 465 } 466 $result = null; 467 if (self::is_function_enabled('exec')) { 468 $exeSniffer = new ExecutableFinder(); 469 // Returns null if nothing is found 470 $result = $exeSniffer->find($program, null, ['/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin', '/bin', '/opt/bin']); 471 } 472 // store the value for 5 minutes 473 $memcache->set($program, $result, 300); 474 return $result; 475 } 476 477 /** 478 * Calculate the disc space for the given path 479 * 480 * BEWARE: this requires that Util::setupFS() was called 481 * already ! 482 * 483 * @param string $path 484 * @param \OCP\Files\FileInfo $rootInfo (optional) 485 * @return array 486 * @throws \OCP\Files\NotFoundException 487 */ 488 public static function getStorageInfo($path, $rootInfo = null) { 489 // return storage info without adding mount points 490 $includeExtStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); 491 492 if (!$rootInfo) { 493 $rootInfo = \OC\Files\Filesystem::getFileInfo($path, $includeExtStorage ? 'ext' : false); 494 } 495 if (!$rootInfo instanceof \OCP\Files\FileInfo) { 496 throw new \OCP\Files\NotFoundException(); 497 } 498 $used = $rootInfo->getSize(); 499 if ($used < 0) { 500 $used = 0; 501 } 502 $quota = \OCP\Files\FileInfo::SPACE_UNLIMITED; 503 $mount = $rootInfo->getMountPoint(); 504 $storage = $mount->getStorage(); 505 $sourceStorage = $storage; 506 if ($storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) { 507 $includeExtStorage = false; 508 $sourceStorage = $storage->getSourceStorage(); 509 } 510 if ($includeExtStorage) { 511 if ($storage->instanceOfStorage('\OC\Files\Storage\Home') 512 || $storage->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage') 513 ) { 514 /** @var \OC\Files\Storage\Home $storage */ 515 $user = $storage->getUser(); 516 } else { 517 $user = \OC::$server->getUserSession()->getUser(); 518 } 519 $quota = OC_Util::getUserQuota($user); 520 if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { 521 // always get free space / total space from root + mount points 522 return self::getGlobalStorageInfo($quota, $user, $mount); 523 } 524 } 525 526 // TODO: need a better way to get total space from storage 527 if ($sourceStorage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) { 528 /** @var \OC\Files\Storage\Wrapper\Quota $storage */ 529 $quota = $sourceStorage->getQuota(); 530 } 531 $free = $sourceStorage->free_space($rootInfo->getInternalPath()); 532 if ($free >= 0) { 533 $total = $free + $used; 534 } else { 535 $total = $free; //either unknown or unlimited 536 } 537 if ($total > 0) { 538 if ($quota > 0 && $total > $quota) { 539 $total = $quota; 540 } 541 // prevent division by zero or error codes (negative values) 542 $relative = round(($used / $total) * 10000) / 100; 543 } else { 544 $relative = 0; 545 } 546 547 $ownerId = $storage->getOwner($path); 548 $ownerDisplayName = ''; 549 $owner = \OC::$server->getUserManager()->get($ownerId); 550 if ($owner) { 551 $ownerDisplayName = $owner->getDisplayName(); 552 } 553 if (substr_count($mount->getMountPoint(), '/') < 3) { 554 $mountPoint = ''; 555 } else { 556 [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4); 557 } 558 559 return [ 560 'free' => $free, 561 'used' => $used, 562 'quota' => $quota, 563 'total' => $total, 564 'relative' => $relative, 565 'owner' => $ownerId, 566 'ownerDisplayName' => $ownerDisplayName, 567 'mountType' => $mount->getMountType(), 568 'mountPoint' => trim($mountPoint, '/'), 569 ]; 570 } 571 572 /** 573 * Get storage info including all mount points and quota 574 */ 575 private static function getGlobalStorageInfo(int $quota, IUser $user, IMountPoint $mount): array { 576 $rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext'); 577 $used = $rootInfo['size']; 578 if ($used < 0) { 579 $used = 0; 580 } 581 582 $total = $quota; 583 $free = $quota - $used; 584 585 if ($total > 0) { 586 if ($quota > 0 && $total > $quota) { 587 $total = $quota; 588 } 589 // prevent division by zero or error codes (negative values) 590 $relative = round(($used / $total) * 10000) / 100; 591 } else { 592 $relative = 0; 593 } 594 595 if (substr_count($mount->getMountPoint(), '/') < 3) { 596 $mountPoint = ''; 597 } else { 598 [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4); 599 } 600 601 return [ 602 'free' => $free, 603 'used' => $used, 604 'total' => $total, 605 'relative' => $relative, 606 'quota' => $quota, 607 'owner' => $user->getUID(), 608 'ownerDisplayName' => $user->getDisplayName(), 609 'mountType' => $mount->getMountType(), 610 'mountPoint' => trim($mountPoint, '/'), 611 ]; 612 } 613 614 /** 615 * Returns whether the config file is set manually to read-only 616 * @return bool 617 */ 618 public static function isReadOnlyConfigEnabled() { 619 return \OC::$server->getConfig()->getSystemValue('config_is_read_only', false); 620 } 621} 622